###_________________________ Lotka-Volterra Model __________________________###
###                         Prey - Predator Model                           ###
# Las ecuaciones de Lotka-Volterra son un modelo biomatemático que pretende responder
# a estas cuestiones prediciendo la dinámica de las poblaciones de presa y depredador
# bajo una serie de hipótesis:
    # * El ecosistema está aislado: no hay migración, no hay otras especies presentes,
    #   no hay plagas...
    # * La población de presas en ausencia de depredadores crece de manera exponencial:
    #   la velocidad de reproducción es proporcional al número de individuos.
    #   Las presas sólo mueren cuando son cazadas por el depredador.
    # * La población de depredadores en ausencia de presas decrece de manera exponencial.
    # * La población de depredadores afecta a la de presas haciéndola decrecer de
    #   forma proporcional al número de presas y depredadores (esto es como decir
    #   de forma proporcional al número de posibles encuentros entre presa y depredador).
    # * La población de presas afecta a la de depredadores también de manera
    #   proporcional al número de encuentros, pero con distinta constante de
    #   proporcionalidad (dependerá de cuanto sacien su hambre los depredadores
    #   al encontrar una presa).

# Se trata de un sistema de dos ecuaciones diferenciales de primer orden, acopladas,
# autónomas y no lineales:

    # $$ \frac{dx}{dt} = \alpha x - \beta x y $$
    # $$ \frac{dy}{dt} = -\gamma y + \delta y x $$

# donde x es el número de presas e y es el número de depredadores.
# Los parámetros son constantes positivas que representan:
    # * $\alpha$: tasa de crecimiento de las presas.
    # * $\beta$: éxito en la caza del depredador.
    # * $\gamma$: tasa de decrecimiento de los depredadores.
    # * $\delta$: éxito en la caza y cuánto alimenta cazar una presa al depredador.
#_____________________________________________________________________________#
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint

# System of 1st order ODEs
def df_dt(x, t, a, b, c, d):

    dx = a * x[0] - b * x[0] * x[1]
    dy = - c * x[1] + d * x[0] * x[1]

    return np.array([dx, dy])

# Parameters
a = 0.1
b = 0.02
c = 0.3
d = 0.01

# Initial Conditions
x0 = 40
y0 = 9
conds_iniciales = np.array([x0, y0])

# Conditions for integration
tf = 200
N = 800
t = np.linspace(0, tf, N)

# Solve the equation
solucion = odeint(df_dt, conds_iniciales, t, args=(a, b, c, d))

# Plot population in terms of time
plt.style.use('seaborn-talk')

plt.figure("Temporal Evolution", figsize=(8,5))
plt.title("Temporal Evolution")
plt.plot(t, solucion[:, 0], label='Prey')
plt.plot(t, solucion[:, 1], label='Predator')
plt.xlabel('Time')
plt.ylabel('Population')
plt.grid()
plt.legend()
plt.show()

# Plot Prey pop. in terms of Predator pop.
plt.figure("Preys vs Predators", figsize=(8,5))
plt.plot(solucion[:, 0], solucion[:, 1], 'r')
plt.xlabel('Prey')
plt.ylabel('Predator')
plt.grid()
plt.show()

# Plot phase map and direction field

# Podemos pintar el campo de direcciones de nuestras ecuaciones usando la función
# quiver. El tamaño de las flechas se ha normalizado para que todas tengan la misma
# longitud y se ha usado un colormap para representar el módulo.
x_max = np.max(solucion[:,0]) * 1.05
y_max = np.max(solucion[:,1]) * 1.05

x = np.linspace(0, x_max, 25)
y = np.linspace(0, y_max, 25)

xx, yy = np.meshgrid(x, y)
uu, vv = df_dt((xx, yy), 0, a, b, c, d)
norm = np.sqrt(uu**2 + vv**2)
uu = uu / norm
vv = vv / norm

plt.figure("Direction field", figsize=(8,5))
plt.quiver(xx, yy, uu, vv, norm, cmap=plt.cm.gray)
plt.plot(solucion[:, 0], solucion[:, 1], 'r')
plt.xlim(0, x_max)
plt.ylim(0, y_max)
plt.xlabel('Preys')
plt.ylabel('Predators')
plt.grid()
plt.show()

# Plot direction field + Pop vs time
n_max = np.max(solucion) * 1.10

fig, ax = plt.subplots(1,2)

fig.set_size_inches(12,5)

ax[0].quiver(xx, yy, uu, vv, norm, cmap=plt.cm.gray)
ax[0].plot(solucion[:, 0], solucion[:, 1], lw=2, alpha=0.8)
ax[0].set_xlim(0, x_max)
ax[0].set_ylim(0, y_max)
ax[0].set_xlabel('Preys')
ax[0].set_ylabel('Predators')

ax[1].plot(t, solucion[:, 0], label='Preys')
ax[1].plot(t, solucion[:, 1], label='Predators')
ax[1].legend()
ax[1].set_xlabel('Time')
ax[1].set_ylabel('Population')
plt.show()

##_______________________ Effect of initial conditions ______________________##
# Se puede demostrar que a lo largo de las líneas del mapa de fases, como la que
# hemos pintado antes, se conserva la cantidad:
    # $$ C = \alpha \ln{y} - \beta y + \gamma \ln{x} -\delta x $$
# Por tanto, pintando un `contour` de esta cantidad podemos obtener la solución
# para distintos valores iniciales del problema.

def C(x, y, a, b, c, d):
    return a * np.log(y) - b * y + c * np.log(x) - d * x

x = np.linspace(0, x_max, 100)
y = np.linspace(0, y_max, 100)
xx, yy = np.meshgrid(x, y)
constant = C(xx, yy, a, b, c, d)

plt.figure('Various solutions', figsize=(8,5))
plt.contour(xx, yy, constant, 50, cmap=plt.cm.Blues)
plt.xlabel('Preys')
plt.ylabel('Predators')
plt.grid()
plt.show()

# Vemos que estas curvas se van haciendo cada vez más y más pequeñas, hasta que,
# en nuestro caso, colapsarían en un punto en torno a $(30,5)$. Se trata de un
# punto de equilibrio o punto crítico; si el sistema lo alcanzase, no evolucionaría
# y el número de cebras y leones sería constante en el tiempo. El otro punto crítico
# de nuestro sistema es el $(0,0)$. Analizándolos matemáticamente se obtiene que:

# El punto crítico situado en $(0,0)$ es un punto de silla. Al tratarse de un punto
# de equilibrio inestable la extinción de cualquiera de las dos especies en el
# modelo sólo puede conseguirse imponiendo la condición inicial nula.

# El punto crítico situado en $(gamma/delta,alpha/beta)$ es un centro (en este
# caso los autovalores de la matriz del sistema linealizado son ambos imaginarios
# puros, por lo que a priori no se conoce su estabilidad).

fig, ax = plt.subplots(1,2)

fig.set_size_inches(12,5)

ax[0].plot(solucion[:, 0], solucion[:, 1], lw=2, alpha=0.8)
ax[0].scatter(c/d, a/b)
levels = (0.5, 0.6, 0.7, 0.72, 0.73, 0.74, 0.75, 0.76, 0.77, 0.775, 0.78, 0.781)
ax[0].contour(xx, yy, constant, levels, colors='blue', alpha=0.3)
ax[0].set_xlim(0, x_max)
ax[0].set_ylim(0, y_max)
ax[0].set_xlabel('presas')
ax[0].set_ylabel('depredadores')

ax[1].plot(t, solucion[:, 0], label='presa')
ax[1].plot(t, solucion[:, 1], label='depredador')
ax[1].legend()
ax[1].set_xlabel('tiempo')
ax[1].set_ylabel('población')
plt.show()

##___________________________ Improving the model ___________________________##
# Como se puede observar, este modelo tiene algunas deficiencias propias de su
# simplicidad y derivadas de las hipótesis bajo las que se ha formulado. Una
# modificación razonable es cambiar el modelo de crecimiento de las presas en
# ausencia de depredadores, suponiendo que en vez de aumentar de forma exponencial,
# lo hacen según una [función logística](http://es.wikipedia.org/wiki/Funci%C3%B3n_log%C3%ADstica).
# Esta curva crece de forma similar a una exponencial al principio, moderándose
# después y estabilizándose asintóticamente en un valor:

def logistic_curve(t, a=1, m=0, n=1, tau=1):
    e = np.exp(-t / tau)
    return a * (1 + m * e) / (1 + n * e)

x_ = np.linspace(0,10)
plt.figure('función logística', figsize=(8,5))
plt.plot(x_, logistic_curve(x_, 1, m=10, n=100, tau=1))
plt.grid()
plt.plot()

# Podemos observar como esta curva crece de forma similar a una exponencial al
# principio, moderándose después y estabilizándose asintóticamente en un valor.
# Este modelo de crecimiento representa mejor las limitaciones en el número de
# presas debidas al medio (falta de alimento, territorio...). Llevando este modelo
# de crecimiento a las ecuaciones originales se tiene un nuevo sistema en el que
# interviene un parámetro más:
    # $$ \frac{dx}{dt} = (\alpha x - r x^2) - \beta x y $$
    # $$ \frac{dy}{dt} = -\gamma y + \delta y x $$

def df_dt_logistic(x, t, a, b, c, d, r):

    dx = a * x[0] - r * x[0]**2 - b * x[0] * x[1]
    dy = - c * x[1] + d * x[0] * x[1]

    return np.array([dx, dy])

# Parámetros
a = 0.1
b = 0.02
c = 0.3
d = 0.01
r = 0.001

# Condiciones iniciales
x0 = 40
y0 = 9
conds_iniciales = np.array([x0, y0])

# Condiciones para integración
tf = 200
N = 800
t = np.linspace(0, tf, N)

# Solution
solucion_logistic = odeint(df_dt_logistic, conds_iniciales, t, args=(a, b, c, d, r))

n_max = np.max(solucion) * 1.10

fig, ax = plt.subplots(1,2)

fig.set_size_inches(12,5)

x_max = np.max(solucion_logistic[:,0]) * 1.05
y_max = np.max(solucion_logistic[:,1]) * 1.05

x = np.linspace(0, x_max, 25)
y = np.linspace(0, y_max, 25)

xx, yy = np.meshgrid(x, y)
uu, vv = df_dt_logistic((xx, yy), 0, a, b, c, d, r)
norm = np.sqrt(uu**2 + vv**2)
uu = uu / norm
vv = vv / norm

ax[0].quiver(xx, yy, uu, vv, norm, cmap=plt.cm.gray)
ax[0].plot(solucion_logistic[:, 0], solucion_logistic[:, 1], lw=2, alpha=0.8)
ax[0].set_xlim(0, x_max)
ax[0].set_ylim(0, y_max)
ax[0].set_xlabel('presas')
ax[0].set_ylabel('depredadores')

ax[1].plot(t, solucion_logistic[:, 0], label='presa')
ax[1].plot(t, solucion_logistic[:, 1], label='depredador')
ax[1].legend()
ax[1].set_xlabel('tiempo')
ax[1].set_ylabel('población')
plt.grid()
plt.show()

# En este caso se puede observar como el comportamiento deja de ser periódico.
# El punto crítico que antes era un centro, se convierte en un atractor y la
# solución tiende a estabilizarse en un número fijo de presas y depredadores.


"""
The prime module makes available several functions for prime operations.
Included are prime tests, prime lists, and prime factorization.
"""

def isprime(n):
    """Returns True if n is prime. O(sqrt(n))"""
    if n < 2:
        return False
    if n == 2 or n == 3:
        return True
    if n % 2 == 0 or n % 3 == 0:
        return False
    i = 5
    w = 2
    while i * i <= n:
        if n % i == 0:
            return False
        i += w
        w = 6 - w
    return True

def primes_up_to(n):
    """
    Return a numpy.ndarray of bools of all primes <= n.
    Uses Sieve of Eratosthenes, O(n log log n).
    """
    import numpy as np
    if n < 2:
        return np.zeros(n+1, dtype=bool)

    A = np.ones(n+1, dtype=bool)
    A[0] = A[1] = False
    for i in range(2, int(np.sqrt(n))+1):
        if A[i]:
            for j in range(i**2, n+1, i):
                A[j] = False
    return np.where(A)[0]

def primes_dict(n):
    """
    Return a dictionary with the prime factorization of n.
    Format of (key, value) is (prime, multiplicity)
    """
    primfac = {}
    d = 2
    while d*d <= n:
        while n % d == 0:
            if d in primfac:
                primfac[d] += 1
            else:
                primfac[d] = 1
            n /= d
        d += 1
    if n > 1:
        primfac[n] = 1
    return primfac

def primes_list(n):
    "Return a list with the prime factorization of n."
    primfac = []
    d = 2
    while d*d <= n:
        while n % d == 0:
            primfac.append(d)
            n /= d
        d += 1
    if n > 1:
        primfac.append(n)
    return primfac

def prime_stream():
    "An indefinite stream of primes, starting from 2. O(n log log n)."
    import itertools
    yield from (2, 3, 5, 7)
    D = {}
    ps = prime_stream()
    next(ps)
    p = next(ps)
    assert p == 3
    psq = p*p
    for i in itertools.count(9, 2):
        if i in D:      # composite
            step = D.pop(i)
        elif i < psq:   # prime
            yield i
            continue
        else:           # composite, = p*p
            assert i == psq
            step = 2*p
            p = next(ps)
            psq = p*p
        i += step
        while i in D:
            i += step
        D[i] = step


import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
from scipy.fftpack import fft, ifft, fftfreq

# Changing K plot
# Create figure
fig_k = make_subplots(
    rows=1, cols=2,
    shared_xaxes=False,
    horizontal_spacing=0.02,
    specs=[[{'type': 'scatter3d'}, {'type': 'scatter3d'}]])

# Add traces, one for each slider step
k= np.linspace(-10, 10, 21)
phi_0 = 0
sig = 0.5
x=np.arange(-20, 20.01, 0.01)

for k_i in np.arange(-10, 11, 1):
    psi = np.multiply(np.exp(1j*k_i*x), np.sqrt(np.exp(-np.power(x-phi_0, 2)/(2*sig**2))/(sig*np.sqrt(2*np.pi))))
    fig_k.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="black", width=2),
            name="wavefunction",
            x=x,
            y=np.imag(psi),
            z=np.real(psi),
            mode="lines"), row=1, col=1)
    fig_k.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="red", width=1),
            name="real part",
            x=x,
            y=-2*np.ones(psi.shape),
            z=np.real(psi),
            mode="lines"), row=1, col=1)
    fig_k.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="orange", width=1),
            name="imaginary part",
            x= x,
            y= np.imag(psi),
            z= -2*np.ones(psi.shape),
            mode="lines"), row=1, col=1)
    fig_k.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="black", width=1),
            name="imaginary part",
            x= x,
            y= -2*np.ones(psi.shape),
            z= np.abs(psi),
            mode="lines"), row=1, col=1)

for k_i in np.arange(-10, 11, 1):
    psi_fft = fft(np.multiply(np.exp(1j*k_i*x), np.sqrt(np.exp(-np.power(x-phi_0, 2)/(2*sig**2))/(sig*np.sqrt(2*np.pi)))))
    xf = fftfreq(len(x), x[1] - x[0])
    fig_k.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="black", width=2),
            name="wavefunction",
            x=np.concatenate((xf[len(xf)//2+1:], [xf[0]], xf[1:len(xf)//2])),
            y=np.imag(np.concatenate((psi_fft[len(xf)//2+1:],[psi_fft[0]], psi_fft[1:len(xf)//2]))),
            z=np.real(np.concatenate((psi_fft[len(xf)//2+1:],[psi_fft[0]], psi_fft[1:len(xf)//2]))),
            mode="lines"), row=1, col=2)
    fig_k.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="red", width=1),
            name="real part",
            #x=xf,
            x=np.concatenate((xf[len(xf)//2+1:], [xf[0]], xf[1:len(xf)//2])),
            y=-10*np.ones(psi_fft.shape),
            #z=np.real(psi_fft),
            z=np.real(np.concatenate((psi_fft[len(xf)//2+1:],[psi_fft[0]], psi_fft[1:len(xf)//2]))),
            mode="lines"), row=1, col=2)
    fig_k.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="orange", width=1),
            name="imaginary part",
            x=np.concatenate((xf[len(xf)//2+1:], [xf[0]], xf[1:len(xf)//2])),
            #x= xf,
            #y= np.imag(psi_fft),
            y=np.imag(np.concatenate((psi_fft[len(xf)//2+1:],[psi_fft[0]], psi_fft[1:len(xf)//2]))),
            z= -200*np.ones(psi_fft.shape),
            mode="lines"), row=1, col=2)
    fig_k.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="black", width=1),
            name="imaginary part",
            x=np.concatenate((xf[len(xf)//2+1:], [xf[0]], xf[1:len(xf)//2])),
            #x= xf,
            y= -10*np.ones(psi_fft.shape),
            z= np.abs(np.concatenate((psi_fft[len(xf)//2+1:],[psi_fft[0]], psi_fft[1:len(xf)//2]))),
            mode="lines"), row=1, col=2)

fig_k.data[40].visible = True
fig_k.data[41].visible = True
fig_k.data[42].visible = True
fig_k.data[43].visible = True

fig_k.data[(len(fig_k.data))//2+40].visible = True
fig_k.data[(len(fig_k.data))//2+41].visible = True
fig_k.data[(len(fig_k.data))//2+42].visible = True
fig_k.data[(len(fig_k.data))//2+43].visible = True

# Create and add slider
steps = []
for i in range(0, (len(fig_k.data))//2, 4):
    step = dict(
        method="update",
        args=[{"visible": [False] * len(fig_k.data)}],  # layout attribute
        label=str(k[i//4])
    )
    step["args"][0]["visible"][i] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][i+1] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][i+2] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][i+3] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][(len(fig_k.data))//2+i] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][(len(fig_k.data))//2+i+1] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][(len(fig_k.data))//2+i+2] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][(len(fig_k.data))//2+i+3] = True  # Toggle i'th trace to "visible"
    #step["args"][0]["visible"][-1] = True
    #step["args"][0]["visible"][-2] = True
    steps.append(step)

sliders = [dict(
    active=10,
    currentvalue={"prefix": "Wave number: "},
    pad={"t": 33},
    steps=steps
)]

fig_k.update_layout(
    sliders=sliders
)

fig_k.update_layout(
    scene = dict(
        xaxis = dict(nticks=10, range=[-5,5],),
        yaxis = dict(nticks=4, range=[-2,2],),
        zaxis = dict(nticks=4, range=[-2,2],),
        xaxis_title='Phi',
        yaxis_title='Imaginary',
        zaxis_title='Real',))
    #width=1000,
    #margin=dict(r=20, l=10, b=10, t=10))
fig_k.update_layout(
    scene2 = dict(
        xaxis = dict(nticks=10, range=[-3, 3]),
        yaxis = dict(nticks=4, range=[-10,10]),
        zaxis = dict(nticks=4, range=[-200, 200]),
        xaxis_title='Q',
        yaxis_title='Imaginary',
        zaxis_title='Real',))
fig_k.update_layout(scene_aspectmode='manual',
                  scene_aspectratio=dict(x=2, y=1, z=1),
                  scene2_aspectmode='manual',
                  scene2_aspectratio=dict(x=2, y=1, z=1),
                  showlegend=False,
                  margin=dict(l=10, r=10, t=5, b=5),
                  scene_camera=dict(eye=dict(x=1, y=2.4, z=0.4)),
                  scene2_camera=dict(eye=dict(x=1, y=2.4, z=0.4)))

fig_k.show(config = {'displayModeBar': False, 'displaylogo': False, 'scrollZoom': False})
fig_k.write_html("wavefunction_changing_k_with_fft.html", config={'displayModeBar': False, 'displaylogo': False, 'scrollZoom': False})

# Changing sig plot
# Create figure
fig_sig = make_subplots(
    rows=1, cols=2,
    shared_xaxes=True,
    horizontal_spacing=0.02,
    specs=[[{'type': 'scatter3d'}, {'type': 'scatter3d'}]])

# Add traces, one for each slider step
k = 3
sig_arr = np.logspace(-0.8, 0.5, 4)
phi_0 = 0
sig = 1
x=np.arange(-20, 20.01, 0.01)

for sig_i in sig_arr:
    psi = np.multiply(np.exp(1j*k*x), np.sqrt(np.exp(-np.power(x-phi_0, 2)/(2*sig_i**2))/(sig_i*np.sqrt(2*np.pi))))
    fig_sig.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="black", width=2),
            name="wavefunction",
            x=x,
            y=np.imag(psi),
            z=np.real(psi),
            mode="lines"), row=1, col=1)
    fig_sig.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="red", width=1),
            name="real part",
            x=x,
            y=-2*np.ones(psi.shape),
            z=np.real(psi),
            mode="lines"), row=1, col=1)
    fig_sig.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="orange", width=1),
            name="imaginary part",
            x= x,
            y= np.imag(psi),
            z= -2*np.ones(psi.shape),
            mode="lines"), row=1, col=1)
    fig_sig.add_trace(
        go.Scatter3d(
            visible=False,
            x=x,
            y= -2*np.ones(psi.shape),
            z=np.abs(psi),
            name="prob amplitude", mode="lines", line=dict(color='black', width=2)), row=1, col=1)

for sig_i in sig_arr:
    psi_fft = fft(np.multiply(np.exp(1j*k*x), np.sqrt(np.exp(-np.power(x-phi_0, 2)/(2*sig_i**2))/(sig_i*np.sqrt(2*np.pi)))))
    xf = fftfreq(len(x), x[1] - x[0])
    fig_sig.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="black", width=2),
            name="wavefunction",
            x=np.concatenate((xf[len(xf)//2+1:], [xf[0]], xf[1:len(xf)//2])),
            y=np.imag(np.concatenate((psi_fft[len(xf)//2+1:],[psi_fft[0]], psi_fft[1:len(xf)//2]))),
            z=np.real(np.concatenate((psi_fft[len(xf)//2+1:],[psi_fft[0]], psi_fft[1:len(xf)//2]))),
            mode="lines"), row=1, col=2)
    fig_sig.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="red", width=1),
            name="real part",
            x=np.concatenate((xf[len(xf)//2+1:], [xf[0]], xf[1:len(xf)//2])),
            y=-9*np.ones(psi_fft.shape),
            z=np.real(np.concatenate((psi_fft[len(xf)//2+1:],[psi_fft[0]], psi_fft[1:len(xf)//2]))),
            mode="lines"), row=1, col=2)
    fig_sig.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="orange", width=1),
            name="imaginary part",
            x= np.concatenate((xf[len(xf)//2+1:], [xf[0]], xf[1:len(xf)//2])),
            y= np.imag(np.concatenate((psi_fft[len(xf)//2+1:],[psi_fft[0]], psi_fft[1:len(xf)//2]))),
            z= -320*np.ones(psi_fft.shape),
            mode="lines"), row=1, col=2)
    fig_sig.add_trace(
        go.Scatter3d(
            visible=False,
            x=np.concatenate((xf[len(xf)//2+1:], [xf[0]], xf[1:len(xf)//2])),
            y= -9*np.ones(psi_fft.shape),
            z=np.abs(np.concatenate((psi_fft[len(xf)//2+1:],[psi_fft[0]], psi_fft[1:len(xf)//2]))),
            name="prob amplitude", mode="lines", line=dict(color='black', width=2)), row=1, col=2)

# Make 10th trace visible
fig_sig.data[4].visible = True
fig_sig.data[5].visible = True
fig_sig.data[6].visible = True
fig_sig.data[7].visible = True
fig_sig.data[len(fig_sig.data)//2+4].visible = True
fig_sig.data[len(fig_sig.data)//2+5].visible = True
fig_sig.data[len(fig_sig.data)//2+6].visible = True
fig_sig.data[len(fig_sig.data)//2+7].visible = True

# Create and add slider
steps = []
for i in range(0, len(fig_sig.data)//2, 4):
    step = dict(
        method="update",
        args=[{"visible": [False] * len(fig_sig.data)}],  # layout attribute
        label=str(round(sig_arr[i//4],2))
    )
    step["args"][0]["visible"][i] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][i+1] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][i+2] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][i+3] = True
    step["args"][0]["visible"][len(fig_sig.data)//2+i] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][len(fig_sig.data)//2+i+1] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][len(fig_sig.data)//2+i+2] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][len(fig_sig.data)//2+i+3] = True
    steps.append(step)

sliders = [dict(
    active=1,
    currentvalue={"prefix": "Standard diviation: "},
    pad={"t": 33},
    steps=steps
)]

fig_sig.update_layout(
    sliders=sliders
)

fig_sig.update_layout(
    scene = dict(
        xaxis = dict(nticks=10, range=[-5,5],),
        yaxis = dict(nticks=4, range=[-2,2],),
        zaxis = dict(nticks=4, range=[-2,2],),
        xaxis_title='Phi',
        yaxis_title='Imaginary',
        zaxis_title='Real',))
    #margin=dict(r=20, l=10, b=10, t=10))
fig_sig.update_layout(
    scene2 = dict(
        xaxis = dict(nticks=10, range=[-5,5],),
        yaxis = dict(nticks=4, range=[-9, 9],),
        zaxis = dict(nticks=4, range=[-320, 320],),
        xaxis_title='Q',
        yaxis_title='Imaginary',
        zaxis_title='Real',))
fig_sig.update_layout(scene_aspectmode='manual',
    scene_aspectratio=dict(x=2, y=1, z=1),
                  scene2_aspectmode='manual',
                  scene2_aspectratio=dict(x=2, y=1, z=1),
                  showlegend=False,
                  margin=dict(l=10, r=10, t=5, b=5),
                  scene_camera=dict(eye=dict(x=1, y=2.4, z=0.4)),
                  scene2_camera=dict(eye=dict(x=1, y=2.4, z=0.4))
                  #width=1000
                  )

fig_sig.show(config={'displayModeBar': False, 'displaylogo': False, 'scrollZoom': False})
fig_sig.write_html("wavefunction_changing_sig_with_fft.html", config={'displayModeBar': False, 'displaylogo': False, 'scrollZoom': False})

# Changing sig plot
# Create figure
fig_mean = make_subplots(
    rows=1, cols=2,
    shared_xaxes=True,
    horizontal_spacing=0.02,
    specs=[[{'type': 'scatter3d'}, {'type': 'scatter3d'}]])

# Add traces, one for each slider step
k = 3
sig = 1
phi_0 = np.arange(-3, 3.5, 0.5)
sig = 1
x=np.arange(-20, 20.01, 0.01)

for phi_0_i in phi_0:
    psi = np.multiply(np.exp(1j*k*x), np.sqrt(np.exp(-np.power(x-phi_0_i, 2)/(2*sig**2))/(sig*np.sqrt(2*np.pi))))
    fig_mean.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="black", width=2),
            name="wavefunction",
            x=x,
            y=np.imag(psi),
            z=np.real(psi),
            mode="lines"), row=1, col=1)
    fig_mean.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="red", width=1),
            name="real part",
            x=x,
            y=-np.ones(psi.shape),
            z=np.real(psi),
            mode="lines"), row=1, col=1)
    fig_mean.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="orange", width=1),
            name="imaginary part",
            x= x,
            y= np.imag(psi),
            z= -np.ones(psi.shape),
            mode="lines"), row=1, col=1)
    fig_mean.add_trace(
        go.Scatter3d(
            visible=False,
            x=x,
            y= -np.ones(psi.shape),
            z=np.abs(psi),
            name="prob amplitude", mode="lines", line=dict(color='black', width=2)), row=1, col=1)

for phi_0_i in phi_0:
    psi_fft = fft(np.multiply(np.exp(1j*k*x), np.sqrt(np.exp(-np.power(x-phi_0_i, 2)/(2*sig**2))/(sig*np.sqrt(2*np.pi)))))
    xf = fftfreq(len(x), x[1] - x[0])
    fig_mean.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="black", width=2),
            name="wavefunction",
            x=np.concatenate((xf[len(xf)//2+1:], [xf[0]], xf[1:len(xf)//2])),
            y=np.imag(np.concatenate((psi_fft[len(xf)//2+1:],[psi_fft[0]], psi_fft[1:len(xf)//2]))),
            z=np.real(np.concatenate((psi_fft[len(xf)//2+1:],[psi_fft[0]], psi_fft[1:len(xf)//2]))),
            mode="lines"), row=1, col=2)
    fig_mean.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="red", width=1),
            name="real part",
            x=np.concatenate((xf[len(xf)//2+1:], [xf[0]], xf[1:len(xf)//2])),
            y=-290*np.ones(psi_fft.shape),
            z=np.real(np.concatenate((psi_fft[len(xf)//2+1:],[psi_fft[0]], psi_fft[1:len(xf)//2]))),
            mode="lines"), row=1, col=2)
    fig_mean.add_trace(
        go.Scatter3d(
            visible=False,
            line=dict(color="orange", width=1),
            name="imaginary part",
            x= np.concatenate((xf[len(xf)//2+1:], [xf[0]], xf[1:len(xf)//2])),
            y= np.imag(np.concatenate((psi_fft[len(xf)//2+1:],[psi_fft[0]], psi_fft[1:len(xf)//2]))),
            z= -410*np.ones(psi_fft.shape),
            mode="lines"), row=1, col=2)
    fig_mean.add_trace(
        go.Scatter3d(
            visible=False,
            x=np.concatenate((xf[len(xf)//2+1:], [xf[0]], xf[1:len(xf)//2])),
            y= -290*np.ones(psi_fft.shape),
            z=np.abs(np.concatenate((psi_fft[len(xf)//2+1:],[psi_fft[0]], psi_fft[1:len(xf)//2]))),
            name="prob amplitude", mode="lines", line=dict(color='black', width=2)), row=1, col=2)

# Make 10th trace visible
fig_mean.data[24].visible = True
fig_mean.data[25].visible = True
fig_mean.data[26].visible = True
fig_mean.data[27].visible = True
fig_mean.data[len(fig_mean.data)//2+24].visible = True
fig_mean.data[len(fig_mean.data)//2+25].visible = True
fig_mean.data[len(fig_mean.data)//2+26].visible = True
fig_mean.data[len(fig_mean.data)//2+27].visible = True

# Create and add slider
steps = []
for i in range(0, len(fig_mean.data)//2, 4):
    step = dict(
        method="update",
        args=[{"visible": [False] * len(fig_mean.data)}],  # layout attribute
        label=str(phi_0[i//4])
    )
    step["args"][0]["visible"][i] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][i+1] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][i+2] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][i+3] = True
    step["args"][0]["visible"][len(fig_mean.data)//2+i] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][len(fig_mean.data)//2+i+1] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][len(fig_mean.data)//2+i+2] = True  # Toggle i'th trace to "visible"
    step["args"][0]["visible"][len(fig_mean.data)//2+i+3] = True
    steps.append(step)

sliders = [dict(
    active=6,
    currentvalue={"prefix": "Mean: "},
    pad={"t": 33},
    steps=steps
)]

fig_mean.update_layout(
    sliders=sliders
)

fig_mean.update_layout(
    scene = dict(
        xaxis = dict(nticks=10, range=[-5,5],),
        yaxis = dict(nticks=4, range=[-2,2],),
        zaxis = dict(nticks=4, range=[-2,2],),
               xaxis_title='Phi',
        yaxis_title='Imaginary',
        zaxis_title='Real',))
    #margin=dict(r=20, l=10, b=10, t=10))
fig_mean.update_layout(
    scene2 = dict(
        xaxis = dict(nticks=10, range=[-3,3],),
        yaxis = dict(nticks=4, range=[-290, 290],),
        zaxis = dict(nticks=4, range=[-430, 430],),
        xaxis_title='Q',
        yaxis_title='Imaginary',
        zaxis_title='Real',))
fig_mean.update_layout(scene_aspectmode='manual',
    scene_aspectratio=dict(x=2, y=1, z=1),
                  scene2_aspectmode='manual',
                  scene2_aspectratio=dict(x=2, y=1, z=1),
                  showlegend=False,
                  margin=dict(l=10, r=10, t=5, b=5),
                  scene_camera=dict(eye=dict(x=1, y=2.4, z=0.4)),
                  scene2_camera=dict(eye=dict(x=1, y=2.4, z=0.4))
                  #width=1000
                  )

fig_mean.show(config={'displayModeBar': False, 'displaylogo': False, 'scrollZoom': False})
fig_mean.write_html("wavefunction_changing_mu_with_fft.html", config={'displayModeBar': False, 'displaylogo': False, 'scrollZoom': False})


import numpy as np

def compint(P, r, n, t) -> float:
    """
    Compute the compounding interest
    P: principal
    r: interest rate
    n: number of times interest compounds in period
    t: Number of time periods

    A = P (1 + r / n) ^ (n * t)
    """
    A = P * np.power(1 + r / n, n * t)
    return A

def payment_amount(balance, payment):
    """
    The amount of a payment given a balance is which ever is less
    """
    curr_payment = min(balance, payment)
    return curr_payment

def single_payment(payment, P, r, n=365, t=1/12):
    """
    Make a single payment on a loan after accruing interest for the provided pay period
    :param payment: Payment amount per period
    :param P: principal
    :param r: interest rate
    :param n: number of times interest compounds in period
    :param t: payment frequency
    :return: (new principal, amount paid)
    """
    # Calculate new principal at end of period
    P = compint(P, r, n, t)
    ## Subtract payment
    # Pay payment amount until principal is zero
    curr_pay = payment_amount(P, payment)
    P -= curr_pay

    # Round
    curr_pay = round(curr_pay, 2)
    P = round(P, 2)
    return P, curr_pay

def pay_loan(payment, P, r, n=365, t=1/12, stop=1e6) -> tuple:
    """
    Pay a compound interest loan to extinction
    Default parameters for daily compounding with monthly payments
    :param payment: Payment amount per period
    :param P: principal
    :param r: interest rate
    :param n: number of times interest compounds in period
    :param t: payment frequency
    :param stop: Stop criteria to avoid infinite calculation (default 1 million)
    :return: (balances, payments)
    | balances: balances for each period
    | payments: payments for each period
    """
    # Keep track of balances after each payment
    balances = []
    payments = []
    while P > 0:
        P, curr_pay = single_payment(payment, P, r, n, t)
        assert P <= stop, f'Payments of {money_amount(payment)} have led the balance to reach stopping criteria of' \
                          f' {money_amount(stop)}.'
        balances.append(P)
        payments.append(curr_pay)
    return balances, payments

def money_amount(x: float) -> str:
    """
    Convert a float to a string dollar amount with commas
    Ex. 1000.235 -> $1,000.24
    """
    # Round to hundreds
    rounded = '%.2f' % x
    # Split into dollars and cents
    dollars, cents = rounded.split('.')

    # Add commas every three digits to dollars
    rev_dollars = ''
    count = 0
    for i in dollars[::-1]:
        rev_dollars += i
        count += 1
        # Add comma
        if count == 3:
            rev_dollars += ','
            count = 0

    # Flip reversed representation
    ref_dollars = rev_dollars.rstrip(',')[::-1]

    # Final format
    ref_amt = f'${ref_dollars}.{cents}'
    return ref_amt


import numpy as np
import math
from mpl_toolkits import mplot3d
import matplotlib.pyplot as plt
import sympy

# Дано:
alpha = 1
f = lambda x, y: alpha * (x**2 - y)**2 + (x - 1)**2

X0 = np.array([-1, -2])
eps = 1e-3

Grad_f = lambda x, y: np.array([4*alpha*x*(x**2 - y) + 2*x - 2, alpha*(-2*x**2 + 2*y)])

# Параметры методов:
kappa0 = 1
nu = 0.95
omega = 0.5

def MethodGoldenRatio(f, b, a = 0, e = eps * 1e-1):
    tau = (math.sqrt(5) + 1) / 2
    Ak, Bk = a, b
    lk = Bk - Ak
    Xk1 = Bk - (Bk - Ak) / tau
    Xk2 = Ak + (Bk - Ak) / tau
    y1, y2 = f(Xk1), f(Xk2)
    while lk >= e:
        if y1 >= y2:
            Ak = Xk1
            Xk1 = Xk2
            Xk2 = Ak + Bk - Xk1
            y1 = y2
            y2 = f(Xk2)
        else:
            Bk = Xk2
            Xk2 = Xk1
            Xk1 = Ak + Bk - Xk2
            y2 = y1
            y1 = f(Xk1)
        lk = Bk - Ak
    return (Ak + Bk) / 2

def MethodsGradientDescent(flag):
    fun = lambda X: f(X[0], X[1])
    w = lambda X: -Grad_f(X[0], X[1])
    X = X0
    kappa_k = kappa0
    NormW = []
    Xk = []
    while True:
        Xk.append(X)
        Wk = w(X)
        NormW.append(np.linalg.norm(Wk))
        if NormW[-1] <= eps:
            break
        fk = fun(X)
        if flag == 0:
            phi = lambda kappa: fun(X + kappa * Wk)
            kappa_k = MethodGoldenRatio(phi, 2.5)
            X = X + kappa_k * Wk
        elif flag == 1:
            Xcurr = X + kappa_k * Wk
            while fk - fun(Xcurr) <= omega * kappa_k * NormW[-1]**2:
                kappa_k *= nu
                Xcurr = X + kappa_k * Wk
            X = Xcurr
    return X, Xk, NormW

print()
print('Методы градиентного спуска')
print('Дано:')
print('Целевая функция:        f(x, y) =', f(sympy.Symbol('x'), sympy.Symbol('y')))
print('Начальное приближение:  X0 =', X0)
print('Точность вычисления:    Eps =', eps)

for i in range(2):
    print()
    print('_' * 100)
    if i == 0:
        print('*' * 30, ' Метод наискорейщего спуска ', '*' * 30)
    elif i == 1:
        print('*' * 20, ' Метод градиентного спуска с дроблением шага ', '*' * 20)
    X, Xk, NormW = MethodsGradientDescent(i)
    print('Точка минимума функции:             Xmin =', X)
    print('Значение функции в точке минимума:  f(Xmin) =', f(X[0], X[1]))
    print('Количество итераций:                k =', len(NormW)-1)


'''
rotations.py rotates cartesian points about x,y,z axis and/or specified vector.
See rotationsVid.py for a more extensive example with outputted graphics with rotations.py at work
'''
#Data array format: [[x1,y1,z1],[x2,y2,z2],[x3,y3,z3],...[xn,yn,zn]]
#Specified vector array: [vx,vy,vz]
#Output array format: [[xr1, yr1, zr1],[xr2,yr2,zr2],[xr3,yr3,zr3],...[xrn,yrn,zrn]]
#Theta (angle) input in degrees

import numpy as np #only requirement

#Rotation definitions
def xrotate(positions, theta): # rotation about the x-axis
    theta = np.deg2rad(theta)
    xr = np.array([[1,0,0],[0,np.cos(theta),-np.sin(theta)],[0,np.sin(theta),np.cos(theta)]])
    return np.dot(positions, xr)

def yrotate(positions, theta): # rotation about the y-axis
    theta = np.deg2rad(theta)
    yr = np.array([[np.cos(theta),0,np.sin(theta)],[0,1,0],[-np.sin(theta),0,np.cos(theta)]])
    return np.dot(positions, yr)

def zrotate(positions, theta): # rotation about the z-axis
    theta = np.deg2rad(theta)
    zr = np.array([[np.cos(theta),-np.sin(theta),0],[np.sin(theta),np.cos(theta),0],[0,0,1]])
    return np.dot(positions, zr)

def vrotate(positions, theta, vec): # rotation about specified vector
    vec = normalize(vec)
    theta = np.deg2rad(theta)
    ux = vec[0]
    uy = vec[1]
    uz = vec[2]
    #Utilize known rotation matrix
    rot = np.array([[np.cos(theta) + ux*ux*(1-np.cos(theta)), ux*uy*(1-np.cos(theta))-uz*np.sin(theta), ux*uz*(1-np.cos(theta))+uy*np.sin(theta)],
                    [uy*ux*(1-np.cos(theta))+uz*np.sin(theta), np.cos(theta) + uy*uy*(1-np.cos(theta)), uy*uz*(1-np.cos(theta))-ux*np.sin(theta)],
                   [uz*ux*(1-np.cos(theta))-uy*np.sin(theta), uz*uy*(1-np.cos(theta))+ux*np.sin(theta), np.cos(theta)+uz*uz*(1-np.cos(theta))]])
    return np.dot(positions, rot)

def normalize(vec): #normalize function used in vrotate
    norm = np.linalg.norm(vec)
    return vec/norm

def main():
    #Example code of how to run
    print("Example of 4 points")
    a = np.random.randint(10,size=(4,3))
    print("Array = \n {}".format(a))
    theta = 45
    print("Theta = {} deg".format(theta))
    xrot = xrotate(a, theta)
    yrot = yrotate(a,theta)
    zrot = zrotate(a, theta)
    vec = [1,1,1]
    vrot = vrotate(a, theta, vec)
    print("x-rotation array = \n{}".format(xrot))
    print("y-rotation array = \n{}".format(yrot))
    print("z-rotation array = \n{}".format(zrot))
    print("v-rotation about vector [1,1,1] array = \n{}".format(vrot))

if __name__ == "__main__":
    main()


import numpy as np

def calc_mean_squared_error(y_N, yhat_N):
    ''' Compute the mean squared error given true and predicted values

    Args
    ----
    y_N : 1D array, shape (N,)
        Each entry represents 'ground truth' numeric response for an example
    yhat_N : 1D array, shape (N,)
        Each entry representes predicted numeric response for an example

    Returns
    -------
    mse : scalar float
        Mean squared error performance metric
        .. math:
            mse(y, \hat{y}) = \frac{1}{N} \sum_{n=1}^N (y_n - \hat{y}_n)^2

    Examples
    --------
    >>> y_N = np.asarray([-2, 0, 2], dtype=np.float64)
    >>> yhat_N = np.asarray([-4, 0, 2], dtype=np.float64)
    >>> calc_mean_squared_error(y_N, yhat_N)
    1.3333333333333333
    '''

    if yhat_N.shape[0] == 0:
        return 0
    return np.sum((yhat_N - y_N)**2)/yhat_N.shape[0]

# y_N = np.asarray([-2, 0, 2], dtype=np.float64)
# yhat_N = np.asarray([-4, 0, 2], dtype=np.float64)
# print(calc_mean_squared_error(y_N, yhat_N))

def calc_mean_absolute_error(y_N, yhat_N):
    ''' Compute the mean absolute error given true and predicted values

    Args
    ----
    y_N : 1D array, shape (N,)
        Each entry represents 'ground truth' numeric response for an example
    yhat_N : 1D array, shape (N,)
        Each entry representes predicted numeric response for an example

    Returns
    -------
    mae : scalar float
        Mean absolute error performance metric
        .. math:
            mae(y, \hat{y}) = \frac{1}{N} \sum_{n=1}^N | y_n - \hat{y}_n |

    Examples
    --------
    >>> y_N = np.asarray([-2, 0, 2], dtype=np.float64)
    >>> yhat_N = np.asarray([-4, 0, 2], dtype=np.float64)
    >>> calc_mean_absolute_error(y_N, yhat_N)
    0.6666666666666666
    '''
    if yhat_N.shape[0] == 0:
        return 0
    return np.sum(abs(yhat_N - y_N))/yhat_N.shape[0]

# y_N = np.asarray([-2, 0, 2], dtype=np.float64)
# yhat_N = np.asarray([-4, 0, 2], dtype=np.float64)
# print(calc_mean_absolute_error(y_N, yhat_N))


"""Implementation of the Logistic sigmoid function
    Reference: https://en.wikipedia.org/wiki/Logistic_function

    Logistic functions are often used in neural networks to introduce
    nonlinearity in the model or to clamp signals to within a specified
    range. A popular neural net element computes a linear combination of
    its input signals, and applies a bounded sigmoid function to the result;
    this model can be seen as a "smoothed" variant of the classical threshold
    neuron.

"""

from .activation_function import ActivationFunction
import numpy as np

class Sigmoid(ActivationFunction):

    def __init__(self):
        super().__init__()

    @classmethod
    def eval(cls, x: np.float) -> np.float:
        """Evaluates the sigmoid function for a given x value
           Reference: https://en.wikipedia.org/wiki/Logistic_function#Derivative
        """
        return 1 / (1 + np.exp(-x))

    @classmethod
    def derivative(cls, x: np.float) -> np.float:
        """Evaluates the derivative of the logitistic function for a given x value
           Reference: https://en.wikipedia.org/wiki/Logistic_function#Derivative
        """
        return cls.eval(x) * (1 - cls.eval(x))


#Function to apply circle intersection
    #Inputs:
        #point 1, distance 1, point 2, distance 2
        #np.a, float, np.a, float

#Standard imports
import math
import numpy as np

def circleIntersection(point_1,dist_1,point_2,dist_2): #*1

    x_0 = point_1[0]
    y_0 = point_1[1]
    r_0 = dist_1

    x_1 = point_2[0]
    y_1 = point_2[1]
    r_1 = dist_2

    d = math.sqrt((x_1-x_0)**2 +(y_1-y_0)**2)
    a = (r_0**2 - r_1**2 + d**2)/(2*d)
    h = np.sqrt(r_0**2 - a**2)

    x_2 = x_0+a*(x_1-x_0)/d
    y_2 = y_0+a*(y_1-y_0)/d

    x_31=x_2 + h*(y_1-y_0)/d
    y_31=y_2 - h*(x_1-x_0)/d

    x_32=x_2 - h*(y_1-y_0)/d
    y_32=y_2 + h*(x_1-x_0)/d

    return (x_31,y_31),(x_32,y_32)

#NOTES: Does not test for irregular conditions, which really shouldn't happen.
    #It should be mathmatically impossible
    #But they keep hapending, so it is critical to do so

#SOURCES: https://stackoverflow.com/questions/3349125/circle-circle-intersection-points *1


import numpy as np

# Pearson's correlation coefficient
# r = summation[(x - x_mean)(y - y_mean)] / sqrt[summation((x - x_mean)^2) * summation((y - y_mean)^2)]
# http://www.datasciencemadesimple.com/wp-content/uploads/2017/07/CORRELATION-COEFFICIENT-FORMULA.png
def pearson_correlation(feature_1, feature_2, generate_sd=False):
    feature_1_mean = np.mean(feature_1, axis=0)
    feature_2_mean = np.mean(feature_2, axis=0)
    feature_1_minus_mean = feature_1 - feature_1_mean
    feature_2_minus_mean = feature_2 - feature_2_mean
    feature_1_minus_mean_squared = np.square(feature_1_minus_mean)
    feature_2_minus_mean_squared = np.square(feature_2_minus_mean)
    features_diff_product = np.multiply(feature_1_minus_mean, feature_2_minus_mean)
    coefficient = (np.sum(features_diff_product, axis=0) /
                   np.sqrt(np.multiply(np.sum(feature_1_minus_mean_squared, axis=0),
                                       np.sum(feature_2_minus_mean_squared, axis=0))))
    if generate_sd:
        feature_1_sd = np.sqrt(np.sum(feature_1_minus_mean_squared)/(np.size(feature_1_minus_mean_squared) - 1))
        feature_2_sd = np.sqrt(np.sum(feature_2_minus_mean_squared)/(np.size(feature_2_minus_mean_squared) - 1))
        return coefficient, feature_1_sd, feature_2_sd
    return coefficient

# outcome/dependent feature residual (error) function
def residual_error(actual_y_matrix, predicted_y_matrix):
    return np.subtract(actual_y_matrix, predicted_y_matrix)

# Sum of squared residuals
def rss(residual_vector):
    return np.sum(np.square(residual_vector), axis=0)

def l2_norm(vector):
    return np.sqrt(np.sum(np.square(vector)))

# sigmoid / logistic function
def sigmoid(x):
    return 1.0 / (1.0 + np.exp(-x))

# softmax function (returns probability of a class)
# input is most commonly product of parameters (theta) and variables (values)
def softmax(x):
    return np.exp(x)/np.sum(np.exp(x), axis=0)


# calculate_pi.py
# MIT License
# github.com/viktor40/HammerBotPython

"""
Calculate the value of π using the Leibniz formula for pi: https://en.wikipedia.org/wiki/Leibniz_formula_for_%CF%80.
This is a rather slow way to calculate π since the series converges slowly. This was mostly a personal exercise.

ITERATIONS will determine the number of iterations to perform to calculate the value of pi.
"""

import matplotlib.pyplot as plt
import numpy as np

ITERATIONS = 50
PLOT = True
x, y = [], []

def calculate_pi(iterations: int):
    """
    Use leibniz series to calculate pi.
    :param iterations: sets the number of iterations to be executed. After this has been reach the program will stop
    :param plot: set if you want to plot the values
    :yield: the function will yield the nth iteration of the leibniz series, together with n.
    """
    pi_ = 0
    for n_ in range(0, iterations):
        pi_ += 4 * ((-1) ** n_) / (2 * n_ + 1)
        yield pi_, n_

pi_iterator = calculate_pi(ITERATIONS)
for value in pi_iterator:
    pi, n = value
    print("After {} iteration{}, the value of π equals {}".format(n + 1, "s" if n else "", pi))

    if PLOT:
        x.append(n)
        y.append(pi)

if PLOT:
    plt.plot(x, y, color='blue')
    plt.plot([0, ITERATIONS], [np.pi, np.pi], color='red')
    plt.xlabel('Number of iterations n')
    plt.ylabel('Value of pi')
    plt.show()


# Python script of the basic operations numpy
# Author: Sandeep Mewara
# Location: Learn By Insight
# Github: https://github.com/samewara/python-examples/blob/master/numpy-basic.py
# #####################################
# To add a new cell, type '# %%'
# To add a new markdown cell, type '# %% [markdown]'
# %% [markdown]
# ## Numpy (Numerical python)
#
# Packages for numerical computation designed for efficiency to work on large data sets
# %% [markdown]
# *** Initalizing Matrix - via LIST ***

# %%
import numpy as np

# %%
#mat = np.array([1, 2, 3, 4, 5])

lst = [1,2,3,4,5]
mat = np.array(lst)
mat

# %%
print("mat.shape:", mat.shape)
print("mat[2]:", mat[2])
print("mat.flatten():", mat.flatten())

# %% [markdown]
# *** Initializing Matrix - via NULL MATRIX (ZEROS) ***

# %%
# 5x4 numpy array where the ith row and jth col has the entry i+j
x,y=5,4
matij = np.zeros((x,y))
matij

# %%
for i in range(x):
    for j in range(y):
        matij[i][j] = i+j

matij

# %% [markdown]
# *** Initializing Matrix - via IDENTITY MATRX ***

# %%
size=3
mat_i = np.identity(size)
mat_i

# %% [markdown]
# *** Initializing Matrix - via ONES MATRIX ***

# %%
m,n=3,2
mat_1=np.ones((m,n))
mat_1

# %% [markdown]
# *** Transpose ***

# %%
mat_1=np.ones((m,n))
mat_1_transpose = mat_1.T
mat_1_transpose

# %% [markdown]
# *** Reshape ***

# %%
mat_1=np.ones((m,n))
mat_1_reshape = mat_1.reshape(1,6) #size is still the same 3*2 = 1*6
mat_1_reshape

# %% [markdown]
# *** Indexing ***

# %%
# An array of grand slam information
Players = ['Roger Federer', 'Margaret Court', 'Rafael Nadal', 'Maria Sharapova', 'Pete Sampras', 'Steffi Graf', 'Novak Djokovic']
Titles = [20, 24, 17, 23, 15, 22, 14]
Finals = [30, 29, 25, 29, 24, 31, 19]

grand_slam=np.array([Players,Titles,Finals])
grand_slam

# %%
# select all players from the matrix
grand_slam[0,::1] #0th row & all column, then the indexing of list :: matrix[row, column], row, column are list so start:stop:step

# %%
# select alternate players with data and ten transpose for easy reading

grand_slam[:,::2].T #all of row and all of columns but step jump 2

# %% [markdown]
# *** Simulation #1 ***
# Simulate the process of rolling a 6 faced die 100 times and find the probability of getting a number that is divisible by 3.

# %%

trials = 100
draws = np.random.randint(1,7,trials) # Generate 10 random integers in the range 1-6
draws

# %%
draws[draws%3==0] #masking - give all the values back that satisfy the mask in the list

# %%
probab_3 = len(draws[draws%3==0])/trials # or can use .size of the list instead of using len
print("probability for numbers divisble by 3:", probab_3)

# %%
# simulation with multiple counts
trials_multiple = 100,1000,10000,100000
for i in range(len(trials_multiple)):
    draws = np.random.randint(1,7,trials_multiple[i])
    probab_3 = len(draws[draws%3==0])/trials_multiple[i] # or can use .size of the list instead of using len
    print("probability for numbers divisble by 3 in {0} trials:{1}".format(trials_multiple[i], probab_3))

# %% [markdown]
# *** Simulation #2 ***
# Two six-faced dice are rolled. Find the probability that their faces sum to 4.

# %%
trials_multiple = 100,1000,10000,100000
for i in range(len(trials_multiple)):
    draws1, draws2 = np.random.randint(1,7,trials_multiple[i]), np.random.randint(1,7,trials_multiple[i])
    probab_sum4 = len(draws1[draws1+draws2 == 4])/trials_multiple[i] #filter the values
    print("probability for sum be 4 in {0} trials:{1}".format(trials_multiple[i], probab_sum4))

# %% [markdown]
# *** Reading CSV Files ***
#
# data-file: economy_data.CSV
#
# sample data on a quarterly basis on Indian economy sampled from RBI database containing Gross Value added, Forex, Borrowings for the period 2016 - Q1 2018. Find the correlation among different indicators

# %%
f=open('./data-files/numpy/economy_data.csv')
data = f.read().split('\n')
data

# %%
# For better data read
for content in data[:-1]:
    print("{0:>9s}  {1:9s} {2:7s} {3:10s} {4:4s}".format(*(content.split(','))))

# %%
#read data into numpy array
#skip the header and 1st column
data_np = np.genfromtxt('./data-files/numpy/economy_data.csv',delimiter=",",skip_header=1,usecols=[1,2,3,4],dtype="float32")
print("Data shape:",data_np.shape, ", Data size:",data_np.size, ", Data type:",data_np.dtype)
print(data_np)

# %%
# Correlation between Forex & Borrowing

gva = data_np[:,0] #select all rows from 1st column
forex = data_np[:,1]
borrowing = data_np[:,2]
cpi = data_np[:,3]

print("Corrleation coefficients forex,borrowing\n",np.corrcoef(forex,borrowing))

# %%
# Forex Data in Q2 (17-18)

data_np[5,:]

# %%
# rows where cpi is less than 3

data_np[data_np[:,-1] < 3]

# %%
# average cpi for all the years

np.round(np.mean(cpi),2)

# %% [markdown]
# *** Broadcasting ***
#
# When two arrays are of different shapes this describes how the arithmetic works between them.
#
# Arrays are compatible for broadcasting when the trailing dimension match or either of them is of length 1, then the broadcasting is done over the missing dimension or length 1 dimension

# %%
#When trailing dimension match
a=np.array([[1,2,3],[3,4,5]])
b=np.array([8,9,10])
print("shape of a",a.shape," shape of b",b.shape)
a+b #broadcasting on row

# %%
#trailing dimension is 1
b=np.array([[10],[12]])
print("shape of a",a.shape," shape of b",b.shape)
a+b #broadcasting on column

# %%
#vertical/horizontal stacking
a = np.identity(3)
b = np.array([[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9]])
print("vertical:\n",np.vstack((a,b)))
print("horizontal:\n",np.hstack((a,b)))

# %%
np.concatenate([a,b],axis=0) #same as vstack

# %% [markdown]
# *** Image Processing ***

# %%
# show an image using matpotlib
import matplotlib.image as mimg
import matplotlib.pyplot as plt
import numpy as np
get_ipython().run_line_magic('matplotlib', 'inline')
img = mimg.imread('./data-files/numpy/hulk.png') # read an image

original_image_pixels = np.array(img,dtype='float32')  # make an numpy array
print (original_image_pixels) # Note the values are between 0 and 1
plt.imshow(original_image_pixels,cmap='gray'); # use a gray scale color map

# %%
# increase contrast pixel by pixel
contrast_enhanced_pixels = original_image_pixels*3 #increase the contrast; but some values may now be more than 1
contrast_enhanced_pixels[contrast_enhanced_pixels > 1] = 1 #clamp the values to max 1
plt.imshow(contrast_enhanced_pixels,cmap='gray')

# %%


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sun Feb 24 22:44:03 2019

@author: alankar
"""

import numpy as np
import matplotlib.pyplot as plt
from scipy import fftpack

dow = np.loadtxt('dow2.txt')
N = len(dow)
t = np.arange(0,N,1)

sampling = 1/(t[1]-t[0])
f = np.linspace(0,sampling,N//2+1)
dow_fft = np.fft.rfft(dow)
plt.figure(figsize=(13,10))
plt.semilogy(f,np.abs(dow_fft)**2)
plt.grid()
plt.xlabel(r'f',size=18)
plt.ylabel(r'Power Spectrum $P(f)$',size=20)
plt.title(r'FFT[DOW]', size=21)
plt.tick_params(axis='both', which='major', labelsize=15)
plt.tick_params(axis='both', which='minor', labelsize=12)
plt.savefig('5_1.png')
plt.show()

plt.figure(figsize=(13,10))
plt.plot(t,dow,label='Original Data')
plt.grid()
plt.xlabel(r'Days',size=18)
plt.ylabel(r'DOW',size=20)
plt.title(r'DOW Industrial Average', size=21)
plt.tick_params(axis='both', which='major', labelsize=15)
plt.tick_params(axis='both', which='minor', labelsize=12)
plt.savefig('5_2.png')

compress = lambda data,freq,percent_acc: np.piecewise(data,[(freq-freq[0])<=\
                                                       (percent_acc/100)*(freq[-1]-freq[0])],[lambda data: data,0.])

dow_fft2 = compress(dow_fft,f,2)
dow2 = np.fft.irfft(dow_fft2)
plt.plot(t,dow2,label=r'%d percent Fourier modes Accepted'%2)
plt.legend(loc='best', prop={'size': 16})
plt.show()

#---------------------------------------------------------------------------------

f = np.linspace(0,sampling,N)
n = np.arange(0,N,1)
plt.figure(figsize=(13,10))
plt.plot(t,dow,label='Original Data')
plt.grid()
plt.xlabel(r'Days',size=18)
plt.ylabel(r'DOW',size=20)
plt.title(r'DOW Industrial Average', size=21)
plt.tick_params(axis='both', which='major', labelsize=15)
plt.tick_params(axis='both', which='minor', labelsize=12)
dow_dct = fftpack.dct(dow, norm='ortho')
dow_dct2 = compress(dow_dct,f,2)
dow2 = fftpack.idct(dow_dct2, norm='ortho')
plt.plot(t,dow2,label=r'%d percent Cosine Transform modes Accepted'%2)
plt.legend(loc='best', prop={'size': 16})
plt.savefig('5_3.png')
plt.show()


import numpy as np
import matplotlib.pyplot as plt
from numpy.random import sample
import seaborn as sns

from multiple_linear_and_polynomial_regression.polynomial_LR import polynomial_linear_regression

sns.set()

def make_polynomial(X, degrees):
    number_of_inputs = len(X)
    polynomial_data = [np.ones(number_of_inputs)]
    for degree in range(degrees):
        polynomial_data.append(np.power(X, degree+1))

    return np.vstack(polynomial_data).T

def fit_and_display(X, y, sample_number, degrees):
    number_of_inputs = len(X)
    train_samples_indeces = np.random.choice(number_of_inputs, sample_number)
    X_train = X[train_samples_indeces]
    y_train = y[train_samples_indeces]

    plt.figure()
    plt.scatter(X_train, y_train)
    plt.show()

    X_train_polynomial = make_polynomial(X_train, degrees)
    w, _ = polynomial_linear_regression(X_train_polynomial, y_train)

    X_polynomial = make_polynomial(X, degrees)
    y_hat = X_polynomial.dot(w)

    plt.figure()
    plt.plot(X,y, label='original data')
    plt.plot(X,y_hat, label='predicted data')
    plt.plot(X_train, y_train, 'r*', label='train data')
    plt.title("Degree = %d" %degrees)
    plt.legend()
    plt.show()

def mse(x, y):
    d = x - y
    return d.dot(d) / len(d)

def plot_train_vs_test_curves(X, y, sample_number=20, max_degrees=20):
    number_of_inputs = len(X)
    train_samples_indeces = np.random.choice(number_of_inputs, sample_number)
    X_train = X[train_samples_indeces]
    y_train = y[train_samples_indeces]

    test_indeces = [index for index in range(number_of_inputs) if index not in train_samples_indeces]
    X_test = X[test_indeces]
    y_test = y[test_indeces]

    MSEs_train = []
    MSEs_test = []

    for degree in range(max_degrees+1):
        X_train_polynomial = make_polynomial(X_train, degree)
        w, y_hat_train = polynomial_linear_regression(X_train_polynomial, y_train)
        mse_train = mse(y_train, y_hat_train)

        MSEs_train.append(mse_train)

        X_test_polynomial = make_polynomial(X_test, degree)
        y_hat_test = X_test_polynomial.dot(w)
        mse_test = mse(y_test, y_hat_test)

        MSEs_test.append(mse_test)

    plt.figure()
    plt.plot(MSEs_train, label='Train MSEs')
    plt.plot(MSEs_test, label='Test MSEs')
    plt.ylabel('MSEs')
    plt.xlabel('Polynomial Degrees')
    plt.legend()
    plt.show()

if __name__ == '__main__':

    number_of_points = 100
    X = np.linspace(0, 6 * np.pi, number_of_points)
    y = np.sin(X)

    plt.figure()
    plt.plot(X,y)
    plt.show()

    for degrees in (5, 6, 7, 8, 9):
        fit_and_display(X, y, sample_number=10, degrees=degrees)

    plot_train_vs_test_curves(X, y)


import numpy as np

def calculate_distance(rA, rB):
    """
    This function calculates the distance between two points

    Parameters
    ----------
    rA, rB : np.ndarray
        The coordinates of each point.

    Returns
    -------
    distance : float
        The distance between two vectors.

    Examples
    --------
    >>> r1 = np.array([0,0,0])
    >>> r2 = np.array([3.0,0,0,])
    >>> calculate_distance(r1_r2)
    3.0
    """

    dist_vec = (rA-rB)
    distance = np.linalg.norm(dist_vec)
    return distance

def calculate_angle(rA, rB, rC, degrees=False):
    # Calculate the angle between three points. Answer is given in radians by default, but can be given in degrees
    # by setting degrees=True
    ab_diff = rA - rB
    bc_diff = rB - rC
    theta = np.arccos(np.dot(ab_diff, bc_diff)/(np.linalg.norm(ab_diff)*np.linalg.norm(bc_diff)))

    if degrees:
        return np.degrees(theta)
    else:
        return theta


import numpy as np

from utils import inc_index, dec_index

def gradient_approximation(f,x,h=1e-8):
    '''
    Numerical approximation of gradient for function f using forward differences.
    Args:
        f (lambda expression): definition of function f.
        x (array): numpy array that holds values where gradient will be computed.
        h (float): step size for forward differences, tipically h=1e-8
    Returns:
        gf (array): numerical approximation to gradient of f.
    '''
    n = x.size
    gf = np.zeros(n)
    f_x = f(x)
    for i in np.arange(n):
        inc_index(x,i,h)
        gf[i] = f(x) - f_x
        dec_index(x,i,h)
    return gf/h
def Hessian_approximation(f,x,h=1e-6):
    '''
    Numerical approximation of Hessian for function f using forward differences.
    Args:
        f (lambda expression): definition of function f.
        x (array): numpy array that holds values where Hessian will be computed.
        h (float): step size for forward differences, tipically h=1e-6
    Returns:
        Hf (array): numerical approximation to Hessian of f.
    '''
    n = x.size
    Hf = np.zeros((n,n))
    f_x = f(x)
    for i in np.arange(n):
        inc_index(x,i,h)
        f_x_inc_in_i = f(x)
        for j in np.arange(i,n):
            inc_index(x,j,h)
            f_x_inc_in_i_j = f(x)
            dec_index(x,i,h)
            f_x_inc_in_j = f(x)
            dif = f_x_inc_in_i_j-f_x_inc_in_i-f_x_inc_in_j+f_x
            Hf[i,j] = dif
            if j != i:
                Hf[j,i] = dif
            dec_index(x,j,h)
            inc_index(x,i,h)
        dec_index(x,i,h)
    return Hf/h**2


import matplotlib.pyplot as plt
import numpy as np

def sigmoid(z):
    return 1.0 / (1.0 + np.exp(-z))

def plot_sigmoid():
	z = np.arange(-7, 7, 0.1)
	phi_z = sigmoid(z)

	plt.plot(z, phi_z)
	plt.axvline(0.0, color='k')
	plt.ylim(-0.1, 1.1)
	plt.xlabel('z')
	plt.ylabel('$\phi (z)$')

	# y axis ticks and gridline
	plt.yticks([0.0, 0.5, 1.0])
	ax = plt.gca()
	ax.yaxis.grid(True)

	plt.tight_layout()
	# plt.savefig('./figures/sigmoid.png', dpi=300)
	plt.show()

def cost_1(z):
    return - np.log(sigmoid(z))

def cost_0(z):
    return - np.log(1 - sigmoid(z))

def plot_cost():
	z = np.arange(-10, 10, 0.1)
	phi_z = sigmoid(z)

	c1 = [cost_1(x) for x in z]
	plt.plot(phi_z, c1, label='J(w) if y=1')

	c0 = [cost_0(x) for x in z]
	plt.plot(phi_z, c0, linestyle='--', label='J(w) if y=0')

	plt.ylim(0.0, 5.1)
	plt.xlim([0, 1])
	plt.xlabel('$\phi$(z)')
	plt.ylabel('J(w)')
	plt.legend(loc='best')
	plt.tight_layout()
	# plt.savefig('./figures/log_cost.png', dpi=300)
	plt.show()

def main():
	plot_sigmoid()
	plot_cost()

if __name__ == '__main__':
    main()


from __future__ import print_function
import numpy as np
import tensorflow as tf

from edward.stats import multinomial
from itertools import product
from scipy.special import gammaln

sess = tf.Session()

def multinomial_logpmf(x, n, p):
    """
    log pmf of multinomial. SciPy doesn't have it.

    Parameters
    ----------
    x: np.array
        vector of length K, where x[i] is the number of outcomes
        in the ith bucket
    n: int
        number of outcomes equal to sum x[i]
    p: np.array
        vector of probabilities summing to 1
    """
    return gammaln(n + 1.0) - \
           np.sum(gammaln(x + 1.0)) + \
           np.sum(x * np.log(p))

def multinomial_entropy(n, p):
    """
    entropy of multinomial. SciPy doesn't have it.

    Parameters
    ----------
    n: int
        number of outcomes equal to sum x[i]
    p: np.array
        vector of probabilities summing to 1
    """
    k = len(p)
    max_range = np.zeros(k, dtype=np.int32) + n
    x = np.array([i for i in product(*(range(i+1) for i in max_range))
                         if sum(i)==n])
    logpmf = [multinomial_logpmf(x[i,:], n, p) for i in range(x.shape[0])]
    return np.sum(np.exp(logpmf) * logpmf)

def multinomial_entropy_vec(n, p):
    """Vectorized version of multinomial_entropy."""
    if isinstance(n, float) or isinstance(n, int):
        return multinomial_entropy(n, p)
    else:
        n_minibatch = n.shape[0]
        return np.array([multinomial_entropy(n[i], p[i, :])
                         for i in range(n_minibatch)])

def _assert_eq(val_ed, val_true):
    with sess.as_default():
        # NOTE: since Tensorflow has no special functions, the values here are
        # only an approximation
        assert np.allclose(val_ed.eval(), val_true, atol=1e-4)

def _test_entropy(n, p):
    val_true = multinomial_entropy_vec(n, p)
    _assert_eq(multinomial.entropy(n, p), val_true)
    _assert_eq(multinomial.entropy(n, tf.constant(p, dtype=tf.float32)), val_true)
    _assert_eq(multinomial.entropy(n, p), val_true)
    _assert_eq(multinomial.entropy(n, tf.constant(p, dtype=tf.float32)), val_true)

def test_entropy_1d():
    _test_entropy(1, np.array([0.5, 0.5]))
    _test_entropy(2, np.array([0.5, 0.5]))
    _test_entropy(3, np.array([0.75, 0.25]))

def test_entropy_2d():
    _test_entropy(np.array([1, 3]), np.array([[0.5, 0.5],[0.75, 0.25]]))
    _test_entropy(np.array([5, 2]), np.array([[0.5, 0.5],[0.75, 0.25]]))


#!/usr/bin/env python

#This script will introduce us to Scipy, a library useful for scientific computation
#Adapted from https://docs.scipy.org/doc/scipy/reference/tutorial/integrate.html

#Integration

#Using known function
print("Integration:")
from scipy.integrate import quad
def integrand(x, a, b):
    return a*x**2 + b

a = 2
b = 1
I = quad(integrand, 0, 1, args=(a,b))
print("Integral of 2x^2 + 1 from x=0..1: {}".format(I[0]))

#Using arbitrarily spaced samples
import numpy as np
def f1(x):
   return x**2

x = np.array([1,3,4])
y1 = f1(x)
from scipy.integrate import simps
I1 = simps(y1, x)
print("Integral of x^2 evaluated at {}: {}".format(x.tolist(), I1))

#Optimization
print("\nOptimization:")
import numpy as np
from scipy.optimize import minimize
def rosen(x):
    """The Rosenbrock function"""
    return sum(100.0*(x[1:]-x[:-1]**2.0)**2.0 + (1-x[:-1])**2.0)

x0 = np.array([1.3, 0.7, 0.8, 1.9, 1.2])
print("Nelder-Mead simplex method:")
res = minimize(rosen, x0, method='nelder-mead', options={'xtol': 1e-8, 'disp': True})

def rosen_der(x):
    xm = x[1:-1]
    xm_m1 = x[:-2]
    xm_p1 = x[2:]
    der = np.zeros_like(x)
    der[1:-1] = 200*(xm-xm_m1**2) - 400*(xm_p1 - xm**2)*xm - 2*(1-xm)
    der[0] = -400*x[0]*(x[1]-x[0]**2) - 2*(1-x[0])
    der[-1] = 200*(x[-1]-x[-2]**2)
    return der

print("Broyden-Fletcher-Goldfarb-Shanno method:")
res1 = minimize(rosen, x0, method='BFGS', jac=rosen_der, options={'disp': True})

#Linear Algebra

print("\nLinear algebra:")

#inverse:
import numpy as np
A = np.array([[1,3,5],[2,5,1],[2,3,8]])
print("A:\n{}\n".format(A))
print("A^(-1):\n{}\n".format(np.linalg.inv(A)))

#Systems solution
A = np.array([[1, 2], [3, 4]])
b = np.array([[5], [6]])
print("A:\n{}\n".format(A))
print("b:\n{}\n".format(b))
print("A^(-1) b:\n{}\n".format(np.linalg.solve(A, b)))
print("det(A): {}".format(np.linalg.det(A)))
print("||A||_inf: {}".format(np.linalg.norm(A, np.inf)))


#
# statistics.py
#

import copy
import math
import numpy
import quicksort

def mean(data):

    return sum(data)/len(data)

def median(data):

    index = len(data)//2
    is_odd = len(data) % 2 > 0
    data = quicksort.sort(copy.deepcopy(data))

    return data[index] if is_odd else (data[index]+data[index-1])/2

def mode(data):

    lookup = {}

    for n in data: lookup[n] = 0
    for n in data: lookup[n] += 1

    return sorted(lookup, key=lookup.__getitem__)[-1]

def standard_deviation(data=[], is_sample=False):

    dmean = mean(data)
    diffs = [math.pow(n-dmean, 2) for n in data]
    divisor = len(data)-1 if is_sample else len(data)

    return math.sqrt(sum(diffs)/divisor)

def variance(data, is_sample=False):

    return math.pow(standard_deviation(data, is_sample), 2)

def covariance(x=[], y=[], is_sample=False):

    xmean = mean(x)
    ymean = mean(y)

    xvect = [(n-xmean) for n in x]
    yvect = [(n-ymean) for n in y]

    divisor = len(x)-1 if is_sample else len(x)
    result = numpy.dot(xvect, yvect) / divisor

    return result

def correlation(x=[], y=[], is_sample=False):

    xstd = standard_deviation(x, is_sample)
    ystd = standard_deviation(y, is_sample)

    return covariance(x, y, is_sample) / xstd / ystd


import numpy as np

def bisection(f,a,b,epsilon=1.0e-6):
	"""Find the root of the function f via bisection where the root lies within [a,b]
	Args:
		f: function to find root of
		a: left-side of interval
		b: right-side of interval
		epsilon: tolerance
	Returns:
		estimate of root
	"""

	assert (b>a)
	assert (f(a)*f(b) < 0)
	delta = b - a
	print("We expect",int(np.ceil(np.log(delta/epsilon)/np.log(2))),"iterations")
	iterations = 0
	while (delta > epsilon):
		c = (a+b)*0.5
		if (f(a)*f(c) < 0):
			b = c
		elif (f(b)*f(c) < 0):
			a=c
		else:
			return c
		delta = b-a
		iterations += 1
	print("It took",iterations,"iterations")
	return c #return midpoint of interval

def false_position(f,a,b,epsilon=1.0e-6):
	"""Find the root of the function f via false position where the root lies within [a,b]
	Args:
		f: function to find root of
		a: left-side of interval
		b: right-side of interval
		epsilon: tolerance
	Returns:
		estimate of root
	"""
	assert (b>a)
	assert (f(a)*f(b) < 0)
	delta = b - a
	iterations = 0
	residual = 1.0
	while (np.fabs(residual) > epsilon):
		m = (f(b)-f(a))/(b-a)
		c = a - f(a)/m
		if (f(a)*f(c) < 0):
			b=c
		elif (f(b)*f(c) < 0):
			a=c
		else:
			print("It took",iterations,"iterations")
			return c
		residual = f(c)
		iterations += 1
	print("It took",iterations,"iterations")
	return c #return c

def ridder(f,a,b,epsilon=1.0e-6):
	"""Find the root of the function f via Ridder's Method where the root lies within [a,b]
	Args:
		f: function to find root of
		a: left-side of interval
		b: right-side of interval
		epsilon: tolerance
	Returns:
		estimate of root
	"""
	assert (b>a)
	assert (f(a)*f(b) < 0)
	delta = b - a
	iterations = 0
	residual = 1.0
	while (np.fabs(residual) > epsilon):
		c = 0.5*(b+a)
		d = 0.0
		if (f(a) - f(b) > 0):
			d = c + (c-a)*f(c)/np.sqrt(f(c)**2-f(a)*f(b))
		else:
			d = c - (c-a)*f(c)/np.sqrt(f(c)**2-f(a)*f(b))
		#now see which part of interval root is in
		if (f(a)*f(d) < 0):
			b=d
		elif (f(b)*f(d) < 0):
			a=d
		residual = f(d)
		iterations += 1
	print("It took",iterations,"iterations")
	return d #return c


import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as st

data = np.loadtxt("dataset.txt")
plt.hist(data)
plt.show()

#Centrality can be defined by average
"""def get_mean(x_input):
  sum = 0
  for x in x_input:
    sum += x
  return sum / len(x_input)
#print(get_mean([2,4,6,8]))
"""
mean = np.mean(data)
print("mean     data.mean()     np.average(data)")
print(mean,data.mean(),np.average(data))

#invoke data.mean() when working with pandas dataframe/ numpy array but not
#on list
#np.average(data) identical to mean but we can also pass in weights
print()
print()
#Median - sort from small to largest and then middle number
#If even no's then average of central two numbers (n/2 + n+1/2)

"""
def get_median(x_inp):
    mid = len(x_inp)//2
    if len(x_inp) %2 == 1:
        return sorted(x_inp)[mid]
    else:
        return 0.5 * np.sum(sorted(x_inp)[mid-1:mid+1])
print(get_median([5,4,2,1,2]))
"""

median = np.median(data)
print("Median")
print(median)
print()
print()
#MEANS ARE SENSETIVE TO OUTLIERS BUT MEDIANS ARE NOT

outlier = np.insert(data,0,5000)
plt.hist(data,label="Data")
plt.axvline(np.mean(data),ls="--",label="Mean Data")
plt.axvline(np.median(data),ls=":",label="Median Data")
plt.axvline(np.mean(outlier),c='r',ls="--",label="Mean Outlier",alpha=0.7)
plt.axvline(np.median(outlier),c='r',ls=":",label="Median Outlier",alpha=0.7)
plt.legend()
plt.xlim(0,20)
plt.show()

#Mode are the most common data in dataset
#If continuous, then bin the data

"""
def get_mode(x):
    values,counts = np.unique(x,return_counts=True)
    max_count_index = np.argmax(counts)
    return values[max_count_index]

print(get_mode[1,7,5,6,3,1,1,2])
"""
print()
mode = st.mode(data)
print("Mode")
print(mode)
print()
print()
#get bins for more numbers

hist,edges = np.histogram(data,bins=100)
edge_centers = 0.5 * (edges[1:] + edges[:-1]) #first to last
mode = edge_centers[hist.argmax()]  #get the index of highest value in histogram
#then get bin center for that and that is mode
print("Mode using KDE")
print(mode)

#if you increase bins amount then mode changes
#to correct above problem - smooth data using scipy in kde

kde = st.gaussian_kde(data)
xvals = np.linspace(data.min(),data.max(),1000)
yval = kde(xvals)
mode = xvals[yval.argmax()]
plt.hist(data,bins=100,density=True,label="Data hist",histtype="step")
plt.plot(xvals,yval,label="KDE")
plt.axvline(mode,label="Mode")
plt.legend()
plt.show()

#with GAUSSIAN Smoothening, now when we change the bin value the kde remains
#same

#TOTAL COMPARISON OF CENTRAL TENDENCY

plt.hist(data,bins=100,label="Data",alpha=0.5)
plt.axvline(mean,label="Mean",ls="--",c='r')
plt.axvline(median,label="Median",ls=":",c='b')
plt.axvline(mode,label="Mode",ls="-",c='g')
plt.legend()
plt.show()
#data is skewed, is the inference


"""
Containing an example for obtaining the gradients through the implicit relation
given by a linear system.

Ax = b

with
J = 0.5 * (x_r - x)^T(x_r - x)

whereas x_r is a reference solution.
"""

import numpy as np
import time

if __name__ == "__main__":

    A = np.array([
        [10, 2, 1],
        [2, 5, 1],
        [1, 1, 3]
    ])

    ##### Creating a reference solution

    b_true = np.array([5, 4, 3])
    x_ref = np.linalg.solve(A, b_true)

    #### [A] Solve the classical system
    b_guess = np.ones(3)

    x = np.linalg.solve(A, b_guess)

    # Evaluate the loss function
    J = 0.5 * (x - x_ref).T @ (x - x_ref)

    #### [B] Obtaining gradients

    ### [3] Adjoint Sensitivities

    time_adjoint = time.time_ns()

    del_J__del_theta = np.zeros((1, 3))
    del_J__del_x = (x - x_ref).T
    d_b__d_theta = np.eye(3)

    # Solve adjoint system
    adjoint_variable = np.linalg.solve(A.T, del_J__del_x.T)

    # Plug in
    d_J__d_theta__adjoint = del_J__del_theta + adjoint_variable.T @ d_b__d_theta

    time_adjoint = time.time_ns() - time_adjoint

    ### [2] Forward Sensitivities

    time_forward = time.time_ns()

    del_J__del_theta = np.zeros((1, 3))
    del_J__del_x = (x - x_ref).T
    d_b__d_theta = np.eye(3)

    # Solve forward system
    d_x__d_theta = np.linalg.solve(A, d_b__d_theta)

    # Plug in
    d_J__d_theta__forward = del_J__del_theta + del_J__del_x @ d_x__d_theta

    time_forward = time.time_ns() - time_forward

    ### [1] Finite differences

    time_finite_differences = time.time_ns()

    eps = 1.0e-6

    d_J__d_theta__finite_difference = np.empty((1, 3))

    for i in range(3):
        b_augmented = b_guess.copy()
        b_augmented[i] += eps

        x_augmented = np.linalg.solve(A, b_augmented)
        J_augmented = 0.5 * (x_augmented - x_ref).T @ (x_augmented - x_ref)

        d_J__d_theta__finite_difference[0, i] = (J_augmented - J) / eps

    time_finite_differences = time.time_ns() - time_finite_differences

    #### Reporting the Results

    print("The loss function")
    print(J)

    print()

    np.set_printoptions(precision=16)
    print("Sensitivities by the Adjoint Method")
    print(d_J__d_theta__adjoint)
    print("Sensitivities by the Forward Method")
    print(d_J__d_theta__forward)
    print("Sensitivities by Finite Differences")
    print(d_J__d_theta__finite_difference)

    print()

    print("Time of the approaches [ns] - lower is better")
    print("Sensitivities by the Adjoint Method")
    print(time_adjoint)
    print("Sensitivities by the Forward Method")
    print(time_forward)
    print("Sensitivities by Finite Differences")
    print(time_finite_differences)


import numpy as np
import numpy.random as np_random

class Solution:
	def monteCarloPiEstimate(self, num_samples=1_000_000):
		"""
		We can estimate pi by randomly picking points in the square
		(0, 0) <= (x, y) <= (1, 1)
		and computing their distance. If their distance is <= 1 then they are
		inside the unit circle. The ratio of the number of points inside the
		unit circle to the number of points picked approximates the ratio of
		the quarter circle and square areas, (pi / 4) : 1. Multiplying the ratio
		by 4 yields an estimate for pi. Ensuring 3 decimal places of accuracy,
		requires at least 1,000,000 points as error is preportional to
		1/(sqrt(num_points))
		"""
		x_samples = np_random.sample(num_samples)
		y_samples = np_random.sample(num_samples)
		samples_within_unit_circle = (x_samples**2 + y_samples**2) <= 1
		return round(
			4 * np.count_nonzero(samples_within_unit_circle) / num_samples, 3
		)

soln = Solution()
print(soln.monteCarloPiEstimate())
print(soln.monteCarloPiEstimate())
print(soln.monteCarloPiEstimate())
print(soln.monteCarloPiEstimate())
print(soln.monteCarloPiEstimate())


# Import functions and libraries
import cv2
import os
import numpy as np
import matplotlib.pyplot as plt
from scipy.fft import dct, idct

# set image file to ../data/00.bmp
# you are free to point to any other image files
IMG_FILE = os.path.join("..", "data", "zelda.bmp")

# read image file, img is a gray scale image
img_gray = cv2.imread(IMG_FILE, cv2.IMREAD_GRAYSCALE)
print(f"Reading {IMG_FILE}, img size(width,height): {img_gray.shape}")

def dct2(a):
    # 2D dct conversion
    # convert image a from spatial domain to frequency domain
    return dct(dct(a.T, norm='ortho').T, norm='ortho')

def idct2(a):
    # 2D idct converstion
    # convert image from freuqency domain back to spatial domain
    return idct(idct(a.T, norm='ortho').T, norm='ortho')

# create a variable to hold dct coefficients
imF = dct2(img_gray)
im_reconstructed = idct2(imF)

# plot original and reconstructed images with matplotlib.pylab
plt.gray()
plt.subplot(121)
plt.imshow(img_gray)
plt.title('original image')
plt.subplot(122)
plt.imshow(im_reconstructed)
plt.title('reconstructed image (DCT+IDCT)')
plt.show()

#compare the pixel value side by side
h, v = 10, 10
orig_pix = img_gray[h, v]
recon_pix = im_reconstructed[h, v]
print(f"pos {h},{v} orig:{orig_pix}, reconstructed:{recon_pix}")

# Question 1:
# think about how to do DCT in a 8x8 block level
# here is how to index an element in numpy
# https://numpy.org/doc/stable/user/basics.indexing.html

# Solution:
'''
img_size = img_gray.shape
# for forward 2d DCT on 8x8 block
dct_8x8 = np.zeros(img_size)
for i in np.r_[:img_size[0]:8]:
    for j in np.r_[:img_size[1]:8]:
        # Apply DCT to the image every 8x8 block of it.
        dct_8x8[i:(i+8), j:(j+8)] = dct(img_gray[i:(i+8), j:(j+8)])

# now inverse 2d DCT on 8x8 block
dct_8x8_reconstructed = np.zeros(img_size)
for i in np.r_[:img_size[0]:8]:
    for j in np.r_[:img_size[1]:8]:
        # Apply inverse DCT to the DCT results every 8x8 block of it.
        dct_8x8_reconstructed[i:(i+8), j:(j+8)
                              ] = idct(dct_8x8[i:(i+8), j:(j+8)])
plt.gray()
plt.subplot(131)
plt.imshow(img_gray)
plt.title('original image')
plt.subplot(132)
plt.imshow(dct_8x8_reconstructed)
plt.title('reconstructed image (DCT+IDCT) 8x8 block')
plt.subplot(133)
plt.imshow(im_reconstructed)
plt.title('reconstructed image (DCT+IDCT)')
plt.show()
'''


import numpy as np
import matplotlib.pyplot as plt
from math import sqrt

def func(x):
    return x**2 - 2*x - 3

def fprime(x):
    return 2*x - 2

def Func(x0):
    x = np.linspace(-5, 7, 100)
    plt.plot(x, func(x))
    plt.plot(x0, func(x0), 'ro')
    plt.xlabel('$x$')
    plt.ylabel('$f(x)$')
    plt.title('Objective Function')

def Path(xs, ys, x0):
    Func(x0)
    plt.plot(xs, ys, linestyle='--', marker='o', color='orange')
    plt.plot(xs[-1], ys[-1], 'ro')
    plt.show()

def GradientDescentSimple(func, fprime, x0, alpha, tol=1e-5, max_iter=1000):
    # initialize x, f(x), and -f'(x)
    xk = x0
    fk = func(xk)
    pk = -fprime(xk)
    # initialize number of steps, save x and f(x)
    num_iter = 0
    curve_x = [xk]
    curve_y = [fk]
    # take steps
    while abs(pk) > tol and num_iter < max_iter:
        # calculate new x, f(x), and -f'(x)
        xk = xk + alpha * pk
        fk = func(xk)
        pk = -fprime(xk)
        # increase number of steps by 1, save new x and f(x)
        num_iter += 1
        curve_x.append(xk)
        curve_y.append(fk)
    # print results
    if num_iter == max_iter:
        print('Gradient descent does not converge.')
    else:
        print('Solution found:\n  y = {:.4f}\n  x = {:.4f}'.format(fk, xk))

    return curve_x, curve_y

xs, ys = GradientDescentSimple(func, fprime, -4, alpha=0.8) # Earlier used alpha = 0.1
Path(xs, ys, -4)

# Solution found:
#   y = -4.0000
#   x = 1.0000


import numpy as np
from numpy.linalg import inv

def setupLHS():
    G1= 3.
    G2= 5.
    a = np.array([[G1+G2, -G2], [-G2, G2]])
    return a

def setupRHS():
    I1= 2.
    I2= 7.
    RHS= np.array([I1, I2])
    return RHS

def checkInverse(a, ainv):
    inverseOK= True
    if not np.allclose(np.dot(a, ainv), np.eye(2)):
        inverseOK= False
    if not np.allclose(np.dot(ainv, a), np.eye(2)):
        inverseOK= False
    if inverseOK:
        print "OK 1 - matrix inverse self-check"
    else:
        print "Not OK 1"

if __name__ == "__main__":
    a= setupLHS()
    RHS= setupRHS()

    ainv = inv(a)
    checkInverse(a, ainv)

    therm= ainv * RHS
    V= np.dot(ainv, RHS)

    HeatSinkPerf= np.array([therm[0][0], therm[1][1]])

    V1RiseDueToI2= therm[0][1]
    V2RiseDueToI1= therm[1][0]

    print HeatSinkPerf[0], V1RiseDueToI2, V[0]
    print HeatSinkPerf[1], V2RiseDueToI1, V[1]

    exactInverse= np.array([[1./3., 1./3.],[1./3., 8./15.]])

    if np.allclose(exactInverse, ainv):
        print "OK 2 - matrix inverse expected value"
    else:
        print "Not OK 2 - matrix inverse expected value"


# ipython --pylab

from scipy.integrate import odeint

def Lorenz(state,t):
  # unpack the state vector
  x = state[0]
  y = state[1]
  z = state[2]

  # these are our constants
  sigma = 10.0
  rho = 28.0
  beta = 8.0/3.0

  # compute state derivatives
  xd = sigma * (y-x)
  yd = (rho-z)*x - y
  zd = x*y - beta*z

  # return the state derivatives
  return [xd, yd, zd]

t = arange(0.0, 30, 0.01)

# original initial conditions
state1_0 = [2.0, 3.0, 4.0]
state1 = odeint(Lorenz, state1_0, t)

# rerun with very small change in initial conditions
delta = 0.0001
state2_0 = [2.0+delta, 3.0, 4.0]
state2 = odeint(Lorenz, state2_0, t)

# animation
figure()
pb, = plot(state1[:,0],state1[:,1],'b-',alpha=0.2)
xlabel('x')
ylabel('y')
p, = plot(state1[0:10,0],state1[0:10,1],'b-')
pp, = plot(state1[10,0],state1[10,1],'b.',markersize=10)
p2, = plot(state2[0:10,0],state2[0:10,1],'r-')
pp2, = plot(state2[10,0],state2[10,1],'r.',markersize=10)
tt = title("%4.2f sec" % 0.00)
# animate
step = 3
for i in xrange(1,shape(state1)[0]-10,step):
  p.set_xdata(state1[10+i:20+i,0])
  p.set_ydata(state1[10+i:20+i,1])
  pp.set_xdata(state1[19+i,0])
  pp.set_ydata(state1[19+i,1])
  p2.set_xdata(state2[10+i:20+i,0])
  p2.set_ydata(state2[10+i:20+i,1])
  pp2.set_xdata(state2[19+i,0])
  pp2.set_ydata(state2[19+i,1])
  tt.set_text("%4.2f sec" % (i*0.01))
  draw()

i = 1939          # the two simulations really diverge here!
s1 = state1[i,:]
s2 = state2[i,:]
d12 = norm(s1-s2) # distance
print ("distance = %f for a %f different in initial condition") % (d12, delta)


from typing import Union, Sequence

import matplotlib.pyplot as plt
import numpy as np
import scipy.interpolate as spinter

Num = Union[int, float]

def piecewise_linear(x_data: Sequence[Num], y_data: Sequence[Num], x_to_find: Num) -> float:
    """
    Shows plot for piece-wise linear with given data and the estimated y-value for the given x value.
    Prints the lowest y-value and its corresponding x-value.
    Will also interpolate and return the y-value for a given x-value.
    """

    plt.plot(x_data, y_data)

    # This does the piece wise interopolation
    for i, x in enumerate(x_data):
        if x_data[i] <= x_to_find < x_data[i+1]:
            y_found = y_data[i] + (y_data[i+1] - y_data[i])/(x_data[i+1] - x_data[i])*(x_to_find - x_data[i])

    plt.plot(x_to_find, y_found, 'r+')
    plt.title("Piece-Wise Interpolation")
    plt.show()

    print(f"Minimum y-value: {min(y_data)}")
    print(f"Corresponding x-value: {x_data[y_data.index(min(y_data))]}")

    return y_found

def exact_data_fit_polynomial(x_data: Sequence[Num], y_data: Sequence[Num], x_to_find: Num, polynomial_order: int) -> Union[float, None]:
    """
    Given: x_data, y_data, and x-value to interpolate, and the polynomial order
    this function will print the coefficients for the specified order interpolation polynomial.
    Polynomial will fit all data points and return the interpolated y-value from the given x-value.
    It will print the minimum y-value and the corresponding x-value for the interpolation function.

    This function returns the interpolated y-value from the given x-value.

    The polynomial order (rank) must be square with the data, otherwise a value of None will be returned.
    """
    if len(x_data) != polynomial_order + 1:  # Checking if the rank is square and we can get an exact solution
        return None

    plt.plot(x_data, y_data, 'r+')

    # We create the polynomial array we need to solve to get the coefficients array
    a_array = []
    for x in x_data:
        inner_temp_array = []
        for exponent in range(polynomial_order+1):
            inner_temp_array.append(x**exponent)
        a_array.append(inner_temp_array)

    coefficients = np.linalg.solve(a_array, y_data)
    print(f"Coefficients: {coefficients}")

    # Doing the interpolation
    y_found = 0
    for i, coeff in enumerate(coefficients):
        y_found += coeff*x_to_find**i

    # It's time for some plotting!
    x_vals = np.linspace(-1, 1, 1000)
    y_vals = []
    for x in x_vals:
        y_temp = 0
        for i, coeff in enumerate(coefficients):
            y_temp += coeff*x**i
        y_vals.append(y_temp)
    plt.plot(x_vals, y_vals)
    plt.plot(x_to_find, y_found, 'g*')
    plt.title(f"{polynomial_order}-Order Polynomial Interpolation Passing Through All Data Points")
    plt.show()

    # Finding the lowest y-value min using a fine grid mesh
    y_min = min(y_vals)
    x_min = x_vals[y_vals.index(min(y_vals))]
    print(f"Minimum y-value found was: {y_min}, corresponding x-value was: {x_min}")

    return y_found

def least_squares_polynomial(x_data: Sequence[Num], y_data: Sequence[Num], x_to_find: Num, polynomial_order: int) -> float:
    """
    Given x-data, y-data, an x-value to interpolate, and a polynomial solution order
    this function will find the least-squares polynomial solution
    and return the interpolated y-value for the given x-value.

    It will print the coefficients for the resulting polynomial.
    It will provide a plot of the polynomial, with the original data points, and the interpolated value.
    Additionally it will print the lowest y-value and its corresponding x-value.
    """
    plt.plot(x_data, y_data, 'r+')

    # Solving for the polynomial coefficients
    a_array = []
    for x in x_data:
        inner_temp_array = []
        for exponent in range(polynomial_order+1):
            inner_temp_array.append(x**exponent)
        a_array.append(inner_temp_array)
    a_array = np.array(a_array)
    a_array_trans = np.matrix.transpose(a_array)
    coefficients = np.linalg.solve(a_array_trans.dot(a_array), a_array_trans.dot(y_data))
    print(f"Coefficients: {coefficients}")

    # Let's find the interpolated y-value
    y_found = 0
    for i, coeff in enumerate(coefficients):
        y_found += coeff*x_to_find**i
    print(y_found)

    # Iterating through the polynomial to provide a plot
    x_vals = np.linspace(-1, 1, 1000)
    y_vals = []
    for x in x_vals:
        y_temp = 0
        for i, coeff in enumerate(coefficients):
            y_temp += coeff*x**i
        y_vals.append(y_temp)
    plt.plot(x_vals, y_vals)
    plt.plot(x_to_find, y_found, 'g*')
    plt.title(f"Least-Squares {polynomial_order}-Order Polynomial Interpolation")
    plt.show()

    # Printing y_min and corresponding x_min
    y_min = min(y_vals)
    x_min = x_vals[y_vals.index(min(y_vals))]
    print(f"Minimum y-value found was: {y_min}, corresponding x-value was: {x_min}")

    return y_found

def cubic_spline(x_data: Sequence[Num], y_data: Sequence[Num], x_to_find: Num) -> float:
    """
    Given x_data, y_data, and an x-value to find, this function will return the
    corresponding y-value using SciPy's cubic spline interpolation.
    It will display a graph showing the data points, interpolation, and the interpolated value.
    It will also print the minimum y-value for the interpolation and its corresponding x-value.
    """
    plt.plot(x_data, y_data)
    x_vals = np.linspace(-1, 1, 1000)
    y_vals = []

    cs = spinter.CubicSpline(x_data, y_data)
    for x in x_vals:
        y_vals.append(cs(x))
    plt.plot(x_vals, y_vals)

    # Let's find the interpolated data point
    y_found = cs(x_to_find)
    plt.plot(x_to_find, y_found, 'g*')
    plt.title("Cubic Spline Interpolation")
    plt.show()

    y_min = min(y_vals)
    x_min = x_vals[y_vals.index(min(y_vals))]
    print(f"Minimum y-value found was: {y_min}, corresponding x-value was: {x_min}")

    return y_found


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sat Nov 25 13:53:40 2017

@author: ratnadeepb
@License: MIT
"""

# System Imports
import numpy as np
import sys

# Local imports
from InnerProductSpaces.dot import dot
from InnerProductSpaces.norm import norm

def angle(u, v, op="radians"):
    n_u = norm(u)
    n_v = norm(v)

    if op not in ("radians", "degrees"):
        sys.exit("At this time we only handle radians and degrees")

    # The angle does not exist if one of them is a zero vector
    if n_u == 0 or n_v == 0:
        return np.NaN

    a = np.arccos(dot(u, v) / (norm(u) * norm(v)))

    if op == "radians":
        return a
    else:
        return a * (180 / np.pi)

if __name__ == "__main__":
    # u = [6, 2]
    u = (-3, 3)
    # v = [1, 4]
    v = (5, 5)
    a_r = angle(u, v)
    if np.isnan(a_r):
        print("The angle does not exist")
    else:
        a_d = angle(u, v, "degrees")
        print("The angle between u and v is {} radians".format(np.round(a_r,
              decimals=4)))
        print("This is the same as {} degrees".format(np.round(a_d,
              decimals=4)))