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


# Tools - NumPy

*NumPy is the fundamental library for scientific computing with Python. NumPy is centered around a powerful N-dimensional array object, and it also contains useful linear algebra, Fourier transform, and random number functions.*

## Creating arrays

Now let's import `numpy`. Most people import it as `np`:

import numpy as np

## `np.zeros`

The `zeros` function creates an array containing any number of zeros:

np.zeros(5)

It's just as easy to create a 2D array (ie. a matrix) by providing a tuple with the desired number of rows and columns. For example, here's a 3x4 matrix:

np.zeros((3,4))

## Some vocabulary

* In NumPy, each dimension is called an **axis**.
* The number of axes is called the **rank**.
    * For example, the above 3x4 matrix is an array of rank 2 (it is 2-dimensional).
    * The first axis has length 3, the second has length 4.
* An array's list of axis lengths is called the **shape** of the array.
    * For example, the above matrix's shape is `(3, 4)`.
    * The rank is equal to the shape's length.
* The **size** of an array is the total number of elements, which is the product of all axis lengths (eg. 3*4=12)

a = np.zeros((3,4))
a

a.shape

a.ndim  # equal to len(a.shape)

a.size

## N-dimensional arrays
You can also create an N-dimensional array of arbitrary rank. For example, here's a 3D array (rank=3), with shape `(2,3,4)`:

np.zeros((2,3,4))

## Array type
NumPy arrays have the type `ndarray`s:

type(np.zeros((3,4)))

## `np.ones`
Many other NumPy functions create `ndarrays`.

Here's a 3x4 matrix full of ones:

np.ones((3,4))

## `np.full`
Creates an array of the given shape initialized with the given value. Here's a 3x4 matrix full of `π`.

np.full((3,4), np.pi)

## `np.empty`
An uninitialized 2x3 array (its content is not predictable, as it is whatever is in memory at that point):

np.empty((2,3))

## np.array
Of course you can initialize an `ndarray` using a regular python array. Just call the `array` function:

np.array([[1,2,3,4], [10, 20, 30, 40]])

## `np.arange`
You can create an `ndarray` using NumPy's `arange` function, which is similar to python's built-in `range` function:

np.arange(1, 5)

It also works with floats:

np.arange(1.0, 5.0)

Of course you can provide a step parameter:

np.arange(1, 5, 0.5)

However, when dealing with floats, the exact number of elements in the array is not always predictible. For example, consider this:

print(np.arange(0, 5/3, 1/3)) # depending on floating point errors, the max value is 4/3 or 5/3.
print(np.arange(0, 5/3, 0.333333333))
print(np.arange(0, 5/3, 0.333333334))

## `np.linspace`
For this reason, it is generally preferable to use the `linspace` function instead of `arange` when working with floats. The `linspace` function returns an array containing a specific number of points evenly distributed between two values (note that the maximum value is *included*, contrary to `arange`):

print(np.linspace(0, 5/3, 6))

## `np.rand` and `np.randn`
A number of functions are available in NumPy's `random` module to create `ndarray`s initialized with random values.
For example, here is a 3x4 matrix initialized with random floats between 0 and 1 (uniform distribution):

np.random.rand(3,4)

Here's a 3x4 matrix containing random floats sampled from a univariate [normal distribution](https://en.wikipedia.org/wiki/Normal_distribution) (Gaussian distribution) of mean 0 and variance 1:

np.random.randn(3,4)

To give you a feel of what these distributions look like, let's use matplotlib (see the [matplotlib tutorial](tools_matplotlib.ipynb) for more details):

%matplotlib inline
import matplotlib.pyplot as plt

plt.hist(np.random.rand(100000), density=True, bins=100, histtype="step", color="blue", label="rand")
plt.hist(np.random.randn(100000), density=True, bins=100, histtype="step", color="red", label="randn")
plt.axis([-2.5, 2.5, 0, 1.1])
plt.legend(loc = "upper left")
plt.title("Random distributions")
plt.xlabel("Value")
plt.ylabel("Density")
plt.show()

## np.fromfunction
You can also initialize an `ndarray` using a function:

def my_function(z, y, x):
    return x * y + z

np.fromfunction(my_function, (3, 2, 10))

NumPy first creates three `ndarrays` (one per dimension), each of shape `(2, 10)`. Each array has values equal to the coordinate along a specific axis. For example, all elements in the `z` array are equal to their z-coordinate:

    [[[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
      [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]]

     [[ 1.  1.  1.  1.  1.  1.  1.  1.  1.  1.]
      [ 1.  1.  1.  1.  1.  1.  1.  1.  1.  1.]]

     [[ 2.  2.  2.  2.  2.  2.  2.  2.  2.  2.]
      [ 2.  2.  2.  2.  2.  2.  2.  2.  2.  2.]]]

So the terms x, y and z in the expression `x * y + z` above are in fact `ndarray`s (we will discuss arithmetic operations on arrays below).  The point is that the function `my_function` is only called *once*, instead of once per element. This makes initialization very efficient.

## Array data
## `dtype`
NumPy's `ndarray`s are also efficient in part because all their elements must have the same type (usually numbers).
You can check what the data type is by looking at the `dtype` attribute:

c = np.arange(1, 5)
print(c.dtype, c)

c = np.arange(1.0, 5.0)
print(c.dtype, c)

Instead of letting NumPy guess what data type to use, you can set it explicitly when creating an array by setting the `dtype` parameter:

d = np.arange(1, 5, dtype=np.complex64)
print(d.dtype, d)

Available data types include `int8`, `int16`, `int32`, `int64`, `uint8`|`16`|`32`|`64`, `float16`|`32`|`64` and `complex64`|`128`. Check out [the documentation](http://docs.scipy.org/doc/numpy-1.10.1/user/basics.types.html) for the full list.

## `itemsize`
The `itemsize` attribute returns the size (in bytes) of each item:

e = np.arange(1, 5, dtype=np.complex64)
e.itemsize

## `data` buffer
An array's data is actually stored in memory as a flat (one dimensional) byte buffer. It is available *via* the `data` attribute (you will rarely need it, though).

f = np.array([[1,2],[1000, 2000]], dtype=np.int32)
f.data

In python 2, `f.data` is a buffer. In python 3, it is a memoryview.

if (hasattr(f.data, "tobytes")):
    data_bytes = f.data.tobytes() # python 3
else:
    data_bytes = memoryview(f.data).tobytes() # python 2

data_bytes

Several `ndarrays` can share the same data buffer, meaning that modifying one will also modify the others. We will see an example in a minute.

## Reshaping an array
## In place
Changing the shape of an `ndarray` is as simple as setting its `shape` attribute. However, the array's size must remain the same.

g = np.arange(24)
print(g)
print("Rank:", g.ndim)

g.shape = (6, 4)
print(g)
print("Rank:", g.ndim)

g.shape = (2, 3, 4)
print(g)
print("Rank:", g.ndim)

## `reshape`
The `reshape` function returns a new `ndarray` object pointing at the *same* data. This means that modifying one array will also modify the other.

g2 = g.reshape(4,6)
print(g2)
print("Rank:", g2.ndim)

Set item at row 1, col 2 to 999 (more about indexing below).

g2[1, 2] = 999
g2

The corresponding element in `g` has been modified.

g

## `ravel`
Finally, the `ravel` function returns a new one-dimensional `ndarray` that also points to the same data:

g.ravel()

## Arithmetic operations
All the usual arithmetic operators (`+`, `-`, `*`, `/`, `//`, `**`, etc.) can be used with `ndarray`s. They apply *elementwise*:

a = np.array([14, 23, 32, 41])
b = np.array([5,  4,  3,  2])
print("a + b  =", a + b)
print("a - b  =", a - b)
print("a * b  =", a * b)
print("a / b  =", a / b)
print("a // b  =", a // b)
print("a % b  =", a % b)
print("a ** b =", a ** b)

Note that the multiplication is *not* a matrix multiplication. We will discuss matrix operations below.

The arrays must have the same shape. If they do not, NumPy will apply the *broadcasting rules*.

## Broadcasting

In general, when NumPy expects arrays of the same shape but finds that this is not the case, it applies the so-called *broadcasting* rules:

## First rule
*If the arrays do not have the same rank, then a 1 will be prepended to the smaller ranking arrays until their ranks match.*

h = np.arange(5).reshape(1, 1, 5)
h

Now let's try to add a 1D array of shape `(5,)` to this 3D array of shape `(1,1,5)`. Applying the first rule of broadcasting!

h + [10, 20, 30, 40, 50]  # same as: h + [[[10, 20, 30, 40, 50]]]

## Second rule
*Arrays with a 1 along a particular dimension act as if they had the size of the array with the largest shape along that dimension. The value of the array element is repeated along that dimension.*

k = np.arange(6).reshape(2, 3)
k

Let's try to add a 2D array of shape `(2,1)` to this 2D `ndarray` of shape `(2, 3)`. NumPy will apply the second rule of broadcasting:

k + [[100], [200]]  # same as: k + [[100, 100, 100], [200, 200, 200]]

Combining rules 1 & 2, we can do this:

k + [100, 200, 300]  # after rule 1: [[100, 200, 300]], and after rule 2: [[100, 200, 300], [100, 200, 300]]

And also, very simply:

k + 1000  # same as: k + [[1000, 1000, 1000], [1000, 1000, 1000]]

## Third rule
*After rules 1 & 2, the sizes of all arrays must match.*

try:
    k + [33, 44]
except ValueError as e:
    print(e)

Broadcasting rules are used in many NumPy operations, not just arithmetic operations, as we will see below.
For more details about broadcasting, check out [the documentation](https://docs.scipy.org/doc/numpy-dev/user/basics.broadcasting.html).

## Upcasting
When trying to combine arrays with different `dtype`s, NumPy will *upcast* to a type capable of handling all possible values (regardless of what the *actual* values are).

k1 = np.arange(0, 5, dtype=np.uint8)
print(k1.dtype, k1)

k2 = k1 + np.array([5, 6, 7, 8, 9], dtype=np.int8)
print(k2.dtype, k2)

Note that `int16` is required to represent all *possible* `int8` and `uint8` values (from -128 to 255), even though in this case a uint8 would have sufficed.

k3 = k1 + 1.5
print(k3.dtype, k3)

## Conditional operators

The conditional operators also apply elementwise:

m = np.array([20, -5, 30, 40])
m < [15, 16, 35, 36]

And using broadcasting:

m < 25  # equivalent to m < [25, 25, 25, 25]

This is most useful in conjunction with boolean indexing (discussed below).

m[m < 25]

##Mathematical and statistical functions

Many mathematical and statistical functions are available for `ndarray`s.

## `ndarray` methods
Some functions are simply `ndarray` methods, for example:

a = np.array([[-2.5, 3.1, 7], [10, 11, 12]])
print(a)
print("mean =", a.mean())

Note that this computes the mean of all elements in the `ndarray`, regardless of its shape.

Here are a few more useful `ndarray` methods:

for func in (a.min, a.max, a.sum, a.prod, a.std, a.var):
    print(func.__name__, "=", func())

These functions accept an optional argument `axis` which lets you ask for the operation to be performed on elements along the given axis. For example:

c=np.arange(24).reshape(2,3,4)
c

c.sum(axis=0)  # sum across matrices

c.sum(axis=1)  # sum across rows

You can also sum over multiple axes:

c.sum(axis=(0,2))  # sum across matrices and columns

0+1+2+3 + 12+13+14+15, 4+5+6+7 + 16+17+18+19, 8+9+10+11 + 20+21+22+23

## Universal functions
NumPy also provides fast elementwise functions called *universal functions*, or **ufunc**. They are vectorized wrappers of simple functions. For example `square` returns a new `ndarray` which is a copy of the original `ndarray` except that each element is squared:

a = np.array([[-2.5, 3.1, 7], [10, 11, 12]])
np.square(a)

Here are a few more useful unary ufuncs:

print("Original ndarray")
print(a)
for func in (np.abs, np.sqrt, np.exp, np.log, np.sign, np.ceil, np.modf, np.isnan, np.cos):
    print("\n", func.__name__)
    print(func(a))

## Binary ufuncs
There are also many binary ufuncs, that apply elementwise on two `ndarray`s.  Broadcasting rules are applied if the arrays do not have the same shape:

a = np.array([1, -2, 3, 4])
b = np.array([2, 8, -1, 7])
np.add(a, b)  # equivalent to a + b

np.greater(a, b)  # equivalent to a > b

np.maximum(a, b)

np.copysign(a, b)

## Array indexing
## One-dimensional arrays
One-dimensional NumPy arrays can be accessed more or less like regular python arrays:

a = np.array([1, 5, 3, 19, 13, 7, 3])
a[3]

a[2:5]

a[2:-1]

a[:2]

a[2::2]

a[::-1]

Of course, you can modify elements:

a[3]=999
a

You can also modify an `ndarray` slice:

a[2:5] = [997, 998, 999]
a

## Differences with regular python arrays
Contrary to regular python arrays, if you assign a single value to an `ndarray` slice, it is copied across the whole slice, thanks to broadcasting rules discussed above.

a[2:5] = -1
a

Also, you cannot grow or shrink `ndarray`s this way:

try:
    a[2:5] = [1,2,3,4,5,6]  # too long
except ValueError as e:
    print(e)

You cannot delete elements either:

try:
    del a[2:5]
except ValueError as e:
    print(e)

Last but not least, `ndarray` **slices are actually *views*** on the same data buffer. This means that if you create a slice and modify it, you are actually going to modify the original `ndarray` as well!

a_slice = a[2:6]
a_slice[1] = 1000
a  # the original array was modified!

a[3] = 2000
a_slice  # similarly, modifying the original array modifies the slice!

If you want a copy of the data, you need to use the `copy` method:

another_slice = a[2:6].copy()
another_slice[1] = 3000
a  # the original array is untouched

a[3] = 4000
another_slice  # similary, modifying the original array does not affect the slice copy

## Multi-dimensional arrays
Multi-dimensional arrays can be accessed in a similar way by providing an index or slice for each axis, separated by commas:

b = np.arange(48).reshape(4, 12)
b

b[1, 2]  # row 1, col 2

b[1, :]  # row 1, all columns

b[:, 1]  # all rows, column 1

**Caution**: note the subtle difference between these two expressions:

b[1, :]

b[1:2, :]

The first expression returns row 1 as a 1D array of shape `(12,)`, while the second returns that same row as a 2D array of shape `(1, 12)`.

## Fancy indexing
You may also specify a list of indices that you are interested in. This is referred to as *fancy indexing*.

b[(0,2), 2:5]  # rows 0 and 2, columns 2 to 4 (5-1)

b[:, (-1, 2, -1)]  # all rows, columns -1 (last), 2 and -1 (again, and in this order)

If you provide multiple index arrays, you get a 1D `ndarray` containing the values of the elements at the specified coordinates.

b[(-1, 2, -1, 2), (5, 9, 1, 9)]  # returns a 1D array with b[-1, 5], b[2, 9], b[-1, 1] and b[2, 9] (again)

## Higher dimensions
Everything works just as well with higher dimensional arrays, but it's useful to look at a few examples:

c = b.reshape(4,2,6)
c

c[2, 1, 4]  # matrix 2, row 1, col 4

c[2, :, 3]  # matrix 2, all rows, col 3

If you omit coordinates for some axes, then all elements in these axes are returned:

c[2, 1]  # Return matrix 2, row 1, all columns.  This is equivalent to c[2, 1, :]

## Ellipsis (`...`)
You may also write an ellipsis (`...`) to ask that all non-specified axes be entirely included.

c[2, ...]  #  matrix 2, all rows, all columns.  This is equivalent to c[2, :, :]

c[2, 1, ...]  # matrix 2, row 1, all columns.  This is equivalent to c[2, 1, :]

c[2, ..., 3]  # matrix 2, all rows, column 3.  This is equivalent to c[2, :, 3]

c[..., 3]  # all matrices, all rows, column 3.  This is equivalent to c[:, :, 3]

## Boolean indexing
You can also provide an `ndarray` of boolean values on one axis to specify the indices that you want to access.

b = np.arange(48).reshape(4, 12)
b

rows_on = np.array([True, False, True, False])
b[rows_on, :]  # Rows 0 and 2, all columns. Equivalent to b[(0, 2), :]

cols_on = np.array([False, True, False] * 4)
b[:, cols_on]  # All rows, columns 1, 4, 7 and 10

## `np.ix_`
You cannot use boolean indexing this way on multiple axes, but you can work around this by using the `ix_` function:

b[np.ix_(rows_on, cols_on)]

np.ix_(rows_on, cols_on)

If you use a boolean array that has the same shape as the `ndarray`, then you get in return a 1D array containing all the values that have `True` at their coordinate. This is generally used along with conditional operators:

b[b % 3 == 1]

## Iterating
Iterating over `ndarray`s is very similar to iterating over regular python arrays. Note that iterating over multidimensional arrays is done with respect to the first axis.

c = np.arange(24).reshape(2, 3, 4)  # A 3D array (composed of two 3x4 matrices)
c

for m in c:
    print("Item:")
    print(m)

for i in range(len(c)):  # Note that len(c) == c.shape[0]
    print("Item:")
    print(c[i])

If you want to iterate on *all* elements in the `ndarray`, simply iterate over the `flat` attribute:

for i in c.flat:
    print("Item:", i)

##Stacking arrays
It is often useful to stack together different arrays. NumPy offers several functions to do just that. Let's start by creating a few arrays.

q1 = np.full((3,4), 1.0)
q1

q2 = np.full((4,4), 2.0)
q2

q3 = np.full((3,4), 3.0)
q3

## `vstack`
Now let's stack them vertically using `vstack`:

q4 = np.vstack((q1, q2, q3))
q4

q4.shape

This was possible because q1, q2 and q3 all have the same shape (except for the vertical axis, but that's ok since we are stacking on that axis).

## `hstack`
We can also stack arrays horizontally using `hstack`:

q5 = np.hstack((q1, q3))
q5

q5.shape

This is possible because q1 and q3 both have 3 rows. But since q2 has 4 rows, it cannot be stacked horizontally with q1 and q3:

try:
    q5 = np.hstack((q1, q2, q3))
except ValueError as e:
    print(e)

## `concatenate`
The `concatenate` function stacks arrays along any given existing axis.

q7 = np.concatenate((q1, q2, q3), axis=0)  # Equivalent to vstack
q7

q7.shape

As you might guess, `hstack` is equivalent to calling `concatenate` with `axis=1`.

## `stack`
The `stack` function stacks arrays along a new axis. All arrays have to have the same shape.

q8 = np.stack((q1, q3))
q8

q8.shape

##Splitting arrays
Splitting is the opposite of stacking. For example, let's use the `vsplit` function to split a matrix vertically.

First let's create a 6x4 matrix:

r = np.arange(24).reshape(6,4)
r

Now let's split it in three equal parts, vertically:

r1, r2, r3 = np.vsplit(r, 3)
r1

r2

r3

There is also a `split` function which splits an array along any given axis. Calling `vsplit` is equivalent to calling `split` with `axis=0`. There is also an `hsplit` function, equivalent to calling `split` with `axis=1`:

r4, r5 = np.hsplit(r, 2)
r4

r5

##Transposing arrays
The `transpose` method creates a new view on an `ndarray`'s data, with axes permuted in the given order.

For example, let's create a 3D array:

t = np.arange(24).reshape(4,2,3)
t

Now let's create an `ndarray` such that the axes `0, 1, 2` (depth, height, width) are re-ordered to `1, 2, 0` (depth→width, height→depth, width→height):

t1 = t.transpose((1,2,0))
t1

t1.shape

By default, `transpose` reverses the order of the dimensions:

t2 = t.transpose()  # equivalent to t.transpose((2, 1, 0))
t2

t2.shape

NumPy provides a convenience function `swapaxes` to swap two axes. For example, let's create a new view of `t` with depth and height swapped:

t3 = t.swapaxes(0,1)  # equivalent to t.transpose((1, 0, 2))
t3

t3.shape

## Linear algebra
NumPy 2D arrays can be used to represent matrices efficiently in python. We will just quickly go through some of the main matrix operations available. For more details about Linear Algebra, vectors and matrics, go through the [Linear Algebra tutorial](math_linear_algebra.ipynb).

## Matrix transpose
The `T` attribute is equivalent to calling `transpose()` when the rank is ≥2:

m1 = np.arange(10).reshape(2,5)
m1

m1.T

The `T` attribute has no effect on rank 0 (empty) or rank 1 arrays:

m2 = np.arange(5)
m2

m2.T

We can get the desired transposition by first reshaping the 1D array to a single-row matrix (2D):

m2r = m2.reshape(1,5)
m2r

m2r.T

## Matrix multiplication
Let's create two matrices and execute a [matrix multiplication](https://en.wikipedia.org/wiki/Matrix_multiplication) using the `dot()` method.

n1 = np.arange(10).reshape(2, 5)
n1

n2 = np.arange(15).reshape(5,3)
n2

n1.dot(n2)

**Caution**: as mentionned previously, `n1*n2` is *not* a matric multiplication, it is an elementwise product (also called a [Hadamard product](https://en.wikipedia.org/wiki/Hadamard_product_(matrices))).

## Matrix inverse and pseudo-inverse
Many of the linear algebra functions are available in the `numpy.linalg` module, in particular the `inv` function to compute a square matrix's inverse:

import numpy.linalg as linalg

m3 = np.array([[1,2,3],[5,7,11],[21,29,31]])
m3

linalg.inv(m3)

You can also compute the [pseudoinverse](https://en.wikipedia.org/wiki/Moore%E2%80%93Penrose_pseudoinverse) using `pinv`:

linalg.pinv(m3)

## Identity matrix
The product of a matrix by its inverse returns the identiy matrix (with small floating point errors):

m3.dot(linalg.inv(m3))

You can create an identity matrix of size NxN by calling `eye`:

np.eye(3)

## QR decomposition
The `qr` function computes the [QR decomposition](https://en.wikipedia.org/wiki/QR_decomposition) of a matrix:

q, r = linalg.qr(m3)
q

r

q.dot(r)  # q.r equals m3

## Determinant
The `det` function computes the [matrix determinant](https://en.wikipedia.org/wiki/Determinant):

linalg.det(m3)  # Computes the matrix determinant

## Eigenvalues and eigenvectors
The `eig` function computes the [eigenvalues and eigenvectors](https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors) of a square matrix:

eigenvalues, eigenvectors = linalg.eig(m3)
eigenvalues # λ

eigenvectors # v

m3.dot(eigenvectors) - eigenvalues * eigenvectors  # m3.v - λ*v = 0

## Singular Value Decomposition
The `svd` function takes a matrix and returns its [singular value decomposition](https://en.wikipedia.org/wiki/Singular_value_decomposition):

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

U, S_diag, V = linalg.svd(m4)
U

S_diag

The `svd` function just returns the values in the diagonal of Σ, but we want the full Σ matrix, so let's create it:

S = np.zeros((4, 5))
S[np.diag_indices(4)] = S_diag
S  # Σ

V

U.dot(S).dot(V) # U.Σ.V == m4

## Diagonal and trace

np.diag(m3)  # the values in the diagonal of m3 (top left to bottom right)

np.trace(m3)  # equivalent to np.diag(m3).sum()

## Solving a system of linear scalar equations

The `solve` function solves a system of linear scalar equations, such as:

* $2x + 6y = 6$
* $5x + 3y = -9$

coeffs  = np.array([[2, 6], [5, 3]])
depvars = np.array([6, -9])
solution = linalg.solve(coeffs, depvars)
solution

Let's check the solution:

coeffs.dot(solution), depvars  # yep, it's the same

Looks good! Another way to check the solution:

np.allclose(coeffs.dot(solution), depvars)

## Vectorization
Instead of executing operations on individual array items, one at a time, your code is much more efficient if you try to stick to array operations. This is called *vectorization*. This way, you can benefit from NumPy's many optimizations.

For example, let's say we want to generate a 768x1024 array based on the formula $sin(xy/40.5)$. A **bad** option would be to do the math in python using nested loops:

import math
data = np.empty((768, 1024))
for y in range(768):
    for x in range(1024):
        data[y, x] = math.sin(x*y/40.5)  # BAD! Very inefficient.

Sure, this works, but it's terribly inefficient since the loops are taking place in pure python. Let's vectorize this algorithm. First, we will use NumPy's `meshgrid` function which generates coordinate matrices from coordinate vectors.

x_coords = np.arange(0, 1024)  # [0, 1, 2, ..., 1023]
y_coords = np.arange(0, 768)   # [0, 1, 2, ..., 767]
X, Y = np.meshgrid(x_coords, y_coords)
X

Y

As you can see, both `X` and `Y` are 768x1024 arrays, and all values in `X` correspond to the horizontal coordinate, while all values in `Y` correspond to the the vertical coordinate.

Now we can simply compute the result using array operations:

data = np.sin(X*Y/40.5)

Now we can plot this data using matplotlib's `imshow` function (see the [matplotlib tutorial](tools_matplotlib.ipynb)).

import matplotlib.pyplot as plt
import matplotlib.cm as cm
fig = plt.figure(1, figsize=(7, 6))
plt.imshow(data, cmap=cm.hot, interpolation="bicubic")
plt.show()

## Saving and loading
NumPy makes it easy to save and load `ndarray`s in binary or text format.

## Binary `.npy` format
Let's create a random array and save it.

a = np.random.rand(2,3)
a

np.save("my_array", a)

Done! Since the file name contains no file extension was provided, NumPy automatically added `.npy`. Let's take a peek at the file content:

with open("my_array.npy", "rb") as f:
    content = f.read()

content

To load this file into a NumPy array, simply call `load`:

a_loaded = np.load("my_array.npy")
a_loaded

## Text format
Let's try saving the array in text format:

np.savetxt("my_array.csv", a)

Now let's look at the file content:

with open("my_array.csv", "rt") as f:
    print(f.read())

This is a CSV file with tabs as delimiters. You can set a different delimiter:

np.savetxt("my_array.csv", a, delimiter=",")

To load this file, just use `loadtxt`:

a_loaded = np.loadtxt("my_array.csv", delimiter=",")
a_loaded

## Zipped `.npz` format
It is also possible to save multiple arrays in one zipped file:

b = np.arange(24, dtype=np.uint8).reshape(2, 3, 4)
b

np.savez("my_arrays", my_a=a, my_b=b)

Again, let's take a peek at the file content. Note that the `.npz` file extension was automatically added.

with open("my_arrays.npz", "rb") as f:
    content = f.read()

repr(content)[:180] + "[...]"

You then load this file like so:

my_arrays = np.load("my_arrays.npz")
my_arrays

This is a dict-like object which loads the arrays lazily:

my_arrays.keys()

my_arrays["my_a"]

## What next?
Now you know all the fundamentals of NumPy, but there are many more options available. The best way to learn more is to experiment with NumPy, and go through the excellent [reference documentation](http://docs.scipy.org/doc/numpy/reference/index.html) to find more functions and features you may be interested in.


# Undergraduate Student: Arturo Burgos
# Professor: João Rodrigo Andrade
# Federal University of Uberlândia - UFU, Fluid Mechanics Laboratory - MFLab, Block 5P, Uberlândia, MG, Brazil

# Fourth exercise: Solving a Linear System --> ax = b

# Here I first set conditions

import numpy as np
from numpy import linalg as lin
np.seterr(divide='ignore', invalid='ignore')

a = np.array([
    [-4, 1, 0, 1, 0, 0, 0, 0, 0],
    [1, -4, 1, 0, 1, 0, 0, 0, 0],
    [0, 1, -4, 0, 0, 1, 0, 0, 0],
    [1, 0, 0, -4, 1, 0, 1, 0, 0],
    [0, 1, 0, 1, -4, 1, 0, 1, 0],
    [0, 0, 1, 0, 1, -4, 0, 0, 1],
    [0, 0, 0, 1, 0, 0, -4, 1, 0],
    [0, 0, 0, 0, 1, 0, 1, -4, 1],
    [0, 0, 0, 0, 0, 1, 0, 1, -4]
    ])

print('The coefficient Matrix is:')
print(a)
print('\n')

b = np.array([-50, -50, -150, 0, 0, -100, -50, -50, -150])

print('The result Matrix is:')
print(b)
print('\n')

x_k = np.zeros(9)
x_k1 = np.ones(9)

# Here I set the tolerance

tolerance = 0.0000000001

# Here I set the iterations

ite = 0

# Here I set the error based in the relative error -> error array

erro = np.ones(9)
erro = (x_k1 - x_k)/x_k1 # the relative error is not normalized so I must use .any() in order to compare with the tolerance

while (erro>tolerance).any() : # use this condition (erro.all()>tolerance) and you will see that the error array is wrong and also
    # the iteration counter gets a way bigger value
    for i in range(0,9):

        x_k1[i] = b[i]

        for j in range(0,9):

            if j!=i:

                x_k1[i] =  x_k1[i] - a[i,j]*x_k[j]

        x_k1[i] =  x_k1[i]/ a[i,i]

    erro = (x_k1 - x_k)/x_k1
    x_k = x_k1.copy()
    ite = ite + 1

print('The number of iterations is: ')
print(ite)
print('\n')

print('The solution is:')
print(x_k1)
print('\n')

print('The error array is given by:')
print(erro)
print('\n')

print(np.max(erro))


import numpy as np
from numpy import linalg as LA
from sympy import *

print("1. Padalinkite intervalą nuo -1.3 iki 2.5 tolygiai į 64 dalis:")
print(np.linspace(-1.3, 2.5, 64))

print("2. Sugeneruokite masyvą dydžio 3n ir užpildykite jį cikliniu šablonu [1, 2, 3]:")
n = 10
print(np.tile(np.array([1, 2, 3]), n))

print("3. Sukurkite masyvą iš pirmųjų 10 nelyginių sveikųjų skaičių:")
n = 10
print(np.arange(1, 2 * n, 2))

print("4. Sukurkite masyvą dydžio 10 x 10 iš nulių ir \"įrėminkite\" jį vienetais:")
n = 10
matrix = np.zeros((n ,n))
matrix[0] = 1
matrix[::, 0:n:n-1] = 1
matrix[n-1] = 1
print(matrix)

print("5. Sukurkite masyvą dydžio 8 x 8, kur 1 ir 0 išdėlioti šachmatine tvarka (panaudokite slicing+striding metodą):")
n = 8
matrix = np.zeros((n ,n))
matrix[::2, 1::2] = 1
matrix[1::2, ::2] = 1
print(matrix)

print("6. Sukurkite masyvą dydžio n×n , kurio (i,j)-oji pozicija lygi i+j:")
n = 10
matrix = np.fromfunction(lambda i, j: i + j, (10, 10), dtype=int)
print(matrix)

print("7. kurkite atsitiktinį masyvą dydžio 3×5 naudodami np.random.rand(3, 5) funkciją ir suskaičiuokite: sumą, eilučių sumą, stulpelių sumą:")
matrix = np.random.rand(3, 5)
print(matrix)
print("Suma: ", matrix.sum())
print("Eilučių suma: ", matrix.sum(axis=0))
print("Stulpelių suma: ", matrix.sum(axis=1))

print("8. Sukurkite atsitiktinį masyvą dydžio 5×5 naudodami np.random.rand(5, 5). Surūšiuokite eilutes pagal antrąjį stulpelį. Tam pamėginkite apjungti masyvo slicing + argsort + indexing metodus:")
matrix = np.random.rand(5, 5)
print("Sukurta matrica: ")
print(matrix)
print("Eilučių rūšiavimo tvarka pagal stulpelius: ")
print(np.argsort(matrix[::, 1], axis=0))
print("Surūšiuota matrica: ")
print(matrix[np.argsort(matrix[::, 1], axis=0), ::])

print("9. Atvirkštinę matricą:")
matrix = np.random.rand(5, 5)
print("Sukurta matrica: ")
print(matrix)
print("Atvirkštinė matrica: ")
print(np.linalg.inv(matrix))

print("10. Apskaičiuokite matricos tikrines reikšmes ir tikrinį vektorių:")
# Teorija: https://mathworld.wolfram.com/Eigenvalue.html
# API: https://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.eig.html
matrix = np.random.rand(2, 2)
print("Sukurta matrica: ")
print(matrix)
eigvalue, eigvector = LA.eig(matrix)
print("Tikrinė reikšmė:")
print(eigvalue)
print("Tikrinis vektorius:")
print(eigvector)

print("11. Pasirinktos funkcijos išvestinę:")
x = Symbol('x')
f = 4*x**3+3
f_derivative = f.diff(x)
print("Funkcija: ", f)
print("Funkcijos išvestinė: ", f_derivative)
f_derivative = lambdify(x, f_derivative)
print("Funkcijos išvestinės reikšmė, kai x = 0: ", f_derivative(0))

print("12. Pasirinktos funkcijos apibrėžtinį ir neapibrėžtinį integralus:")
x = Symbol('x')
f = 4*x**3+3
f_integral = f.integrate(x)
print("Funkcija: ", f)
print("Neapibrėžtas integralas: ", f_integral)
a = 0
b = 1
print("Apibrėžtas integralas ", f_integral,  " (funkcijos", f, ") nuo ", a, " iki ", b, ":", f.integrate((x, a, b)))


import pandas as pd
import numpy as np
import math
import pdb
import random
from math import sqrt

def average(series):

    y = len(series)
    x = sum(series)

    av = x/y
    return(av)

def standard_deviation(series):

    r = 0
    aver = average(series)

    for x in range (len(series)):
        k = abs( series[x] - aver)**2
        r = r + k

    ss = r/(len(series)-1)
    if ss>0 :
        return (sqrt(ss))
    else:
        return(n/a)
    """
    implements the sample standard deviation of a series from scratch
    you may need a for loop and your average function
    also the function math.sqrt
    you should get the same result as calling .std() on your data
    https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.std.html
    See numpy documenation for implementation details:
    https://docs.scipy.org/doc/numpy/reference/generated/numpy.std.html
    """

def median(series):

    l=sorted(series)
    t= len(l)

    if t<1:
        return None
    if t % 2 == 0:
        return (l[int(t/2)] + l[int(t/2)-1]) / 2
    else:
        return l[int((t-1)/2)]

    """
    finds the median of the series from scratch
    you may need to sort your values and use
    modular division
    this number should be the same as calling .median() on your data
    See numpy documenation for implementation details:
    https://docs.scipy.org/doc/numpy/reference/generated/numpy.median.html
    https://pandas.pydata.org/pandas-docs/version/0.23.0/generated/pandas.Series.median.html
    """


#Program used to compute least squared fitting of a set of data X,Y
# Y=A+BX
################
## Conventions
##X: List where data are stored
## SS:sum of x or y squared
## S:sum of x or y
## SXY:sum of xi*yi
## n:number of data
## A:intercept
## B:slope

import numpy as np
def S(X):
	X=np.array(X)
	return np.sum(X)
def SS(X):
	X=np.array(X)
	return np.sum(X**2)
def SXY(X,Y):
	X=np.array(X)
	Y=np.array(Y)
	return np.sum(X*Y)
def delta(X):
	X=np.array(X)
	return len(X)*SS(X)-(S(X))**2
def A(X,Y):
	return (SS(X)*S(Y)-S(X)*SXY(X,Y))/delta(X)
def B(X,Y):
	return (len(X)*SXY(X,Y)-S(X)*S(Y))/delta(X)
def sigma_y(X,Y):
	a=A(X,Y)
	b=B(X,Y)
	X=np.array(X)
	Y=np.array(Y)
	return np.sqrt(SS(Y-a-b*X)/(len(X)-2))
def sigma_A(X,Y):
	return  sigma_y(X,Y)*np.sqrt(SS(X))/delta(X)
def sigma_B(X,Y):
	return  sigma_y(X,Y)*len(X)/delta(X)
def fit(X,Y):
    return "y={:.4f}+{}x".format(A(X,Y),B(X,Y))
#f"y={A(X,Y)}+{B(X,Y)}x"
#"y={:.4f}+{:.4f}x".format(A(X,Y),B(X,Y))
	#return "$\sin \\theta_t=({:.2f}\pm{:.2f})\\\\+({:.2f}\pm{:.2f})\sin \\theta_i$".format(A(X,Y),sigma_A(X,Y),B(X,Y),sigma_B(X,Y))
def y_pred(x,X,Y):
	a=A(X,Y)
	b=B(X,Y)
	x=np.array(x)
	return a+b*x


import numpy as np

# Constants
SCARA_X_OFFSET = 1.035 # meters, indexed from bottom left corner
SCARA_Y_OFFSET = 0.097 # meters, indexed from bottom left corner

r1 = 0.5 # meters, length of the first arm
r2 = 0.5 # meters, length of the second arm

# Positional definitions for SCARA:
#
# Angles of theta_1, theta_2 defined WRT horizontal.
#
# SCARA (0, 0)
#  |
#  v
# theta_1----(r1)------theta_2------(r2)----tip

def fkin(theta_1, theta_2):
    '''
    Returns (x, y) position as a function of the joint angles q0, q1
    '''
    return np.array([
        SCARA_X_OFFSET +r1*np.cos(theta_1) + r2*np.cos(-theta_2),
        SCARA_Y_OFFSET +r1*np.sin(theta_1) + r2*np.sin(-theta_2)])

def ikin(x, y):
    '''
    Returns array of [(theta_1_1, theta_1_2), (theta_2_1, theta_2_2)] as a
    function of the (x, y) position given. Returns
    None if (x, y) is out of bounds or out of reach.
    '''

    # Else, compute the branches
    x = x - SCARA_X_OFFSET
    y = y - SCARA_Y_OFFSET

    # Check if out of reach
    if (np.sqrt((x)**2 + (y)**2) > r1 + r2):
        return None
    # Check if out of bounds
    if (y < 0):
        return None

    # angle from base to desired point
    theta_t = np.arctan2(y, x)

    # calculate the angle at the elbow using law  of cosines
    theta_1_2 = np.arccos((x**2 + y**2 - r1**2 -r2**2)/(2*r1*r2))

    # difference between angles
    theta_1_1 = theta_t - np.arctan2((r1*np.sin(theta_1_2)), (r1 + r2 * np.cos(theta_1_2)))

    # calculate the second set of angles
    theta_2_2 = -1 * theta_1_2

    theta_2_1 = theta_t + (theta_t - theta_1_1)

    return np.array(
        [[theta_1_1, -(theta_1_1+theta_1_2)],
         [theta_2_1, -(theta_2_1+theta_2_2)]])

def jacobian(thetas):
    return np.array(
        [[-r1*np.sin(thetas[0]), -r2*np.sin(thetas[1])],
         [r1*np.cos(thetas[0]), r2*np.cos(thetas[1])]])


from math import sqrt
from numpy import linspace as ls

import matplotlib.pyplot as plt
import matplotlib.patches as mpatch

class Quadratic(object):
    """
    This is a Quadratic function class that takes a quadratic equation in standard form
    and returns anything that you might want.

    X and Y intercepts, vertex of graph, factored form of equation, vertex form of equation,
    graph of function as a vector or raster image (png, or pdf).

    USAGE EXAMPLES:
    >>> q = Quadratic(5, 25, 30)
    >>> print(q)
    f(x) = 5x^2 + 25x + 30
    >>> q.vertex()
    (-2.5, -1.25)
    >>> q.factored_form()
    f(x) = 5(x+2)(x+3)
    """

    def __init__(self, a, b, c):
        self.a = float(a)
        self.b = float(b)
        self.c = float(c)
        self.y_int = (0, self.c)
        self.discriminant = self.b ** 2 - (4 * self.a * self.c)

    def __repr__(self, encode=True):
        """
        Python2 doesn't have default unicode support so I
        have added the encode method but matplotlib doesn't need the encode method
        so I made a default parameter and enter False when I'm using this for matplotlib
        """
        if self.b == 0 and self.c == 0:
            if encode:
                return u'f(x) = %dx\u00b2'.encode('UTF-8') % self.a
            else:
                return u'f(x) = %dx\u00b2' % self.a
        elif self.a == 0:
            if encode:
                return u'f(x) = %dx%+d'.encode('UTF-8') % (self.b, self.c)
            else:
                return u'f(x) = %dx%+d' % (self.b, self.c)
        elif self.b == 0:
            if encode:
                return u'f(x) = %dx\u00b2%+d'.encode('UTF-8') % (self.a, self.c)
            else:
                return u'f(x) = %dx\u00b2%+d' % (self.a, self.c)
        elif self.c == 0:
            if encode:
                return u'f(x) = %dx\u00b2%+dx'.encode('UTF-8') % (self.a, self.b)
            else:
                return u'f(x) = %dx\u00b2%+dx' % (self.a, self.b)
        else:
            if encode:
                return u'f(x) = %dx\u00b2%+dx%+d'.encode('UTF-8') % (self.a, self.b, self.c)
            else:
                return u'f(x) = %dx\u00b2%+dx%+d' % (self.a, self.b, self.c)

    @staticmethod
    def __is_square(num):
        """
        Checks if number is a perfect square.
        I'm adding 0.5 to root because you can never rely on exact comparisons
        when dealing with float numbers.
        Maybe sqrt(49) evaluates to 6.99999 or 7.00001
        so taking the square of the int right off isn't going to work.
        we add 0.5 and then take int() to ensure we have what we want.
        """
        root = sqrt(num)
        if int(root + 0.5) ** 2 == num:
            return True
        else:
            return False

    def has_x_intercept(self):
        """
        According to a mathematical rule:
        1. If the discriminant of a quadratic function is greater than 0,
        the vertex is below x axis, therefore the graph has 2 x-intercepts.
        2. If the discriminant of a quadratic function is equal to 0,
        the vertex is on x axis, therefore the graph has 1 x-intercept.
        3. If the discriminant of a quadratic function is less than 0,
        the vertex is above x axis, therefore the graph doesn't have x-intercepts.
        """
        if self.discriminant > 0:
            return 2
        elif self.discriminant == 0:
            return 1
        else:
            return 0

    def x_intercepts(self):
        """
        If graph has 0 x-intercepts, return None.
        If graph has 1 x-intercept, return vertex.
        If graph has 2 x-intercepts, use quadratic formula to get x-intercepts.
        """
        if self.has_x_intercept() == 0:
            return None
        elif self.has_x_intercept() == 1:
            return (self.vertex()[0],)
        else:
            x1 = (-self.b + sqrt(self.discriminant)) / (2 * self.a)
            x2 = (-self.b - sqrt(self.discriminant)) / (2 * self.a)
            return round(x1, 3), round(x2, 3)

    def is_factorable(self):
        """
        According to a mathematical rule:
        If the discriminant of a quadratic function is a perfect square,
        the equation is factorable.
        """
        if self.discriminant < 0:
            return False
        if self.__is_square(self.discriminant):
            return True
        return False

    def factored_form(self):
        """
        In order to find the factored form, we need to find the value
        of a, which is the vertical stretch coefficient. We can do that
        by evaluating an x point at a certain distance from the vertex,
        then we divide it by the value it would have in an x^2 graph.
        The other factors are easily obtained from the x intercepts.
        EXAMPLE:
        f(x) = 2x^2 + 4x + 2
        vertex = (-1, 0)

        evaluate f(0) = 2
        k) x distance from vertex-x = 1
        k) y distance from vertex-y = 2

        f(1) in an x^2 graph = 1
        h) x distance from vertex-x = 1
        h) y distance from vertex-y = 1

        a = k)dist-y / h)dist-y = 2 / 1 = 2

        Remaining factors:
        (x + (-x_int 1)) * (x + (-x_int 2))
        """
        if self.is_factorable():
            x_ints = self.x_intercepts()
            vertex = self.vertex()
            if len(x_ints) == 1:
                a = self.evaluate(0) / ((0 - x_ints[0]) ** 2)
                return u'f(x) = %d(x%+d)\u00b2'.encode('UTF-8') % (a, -x_ints[0])
            a = self.evaluate(max(x_ints)) - vertex[1] / ((max(x_ints) - vertex[0]) ** 2)
            return u'f(x) = %d(x%+d)(x%+d)'.encode('UTF-8') % (a, -x_ints[0], -x_ints[1])
        else:
            return None

    def vertex(self):
        """
        Using a shortened method of completing the square, I get the x and y of the vertex
        for any given graph f(x) = ax^2 + bx + c
        the vertex form is f(x) = a(x - h)^2 + k
        we can convert using this method:
        EXAMPLE. f(x) = -4x^2 - 32x + 6
        1. factor the coefficients
            f(x) = -4(x^2 + 8x) + 6

        2. create a perfect square trinomial
            f(x) = -4(x^2 + 8x + 16 - 16) + 6

        in the second step we are taking half of the second coefficient in the bracket and squaring it.
        but since we can't change the value of our graph we also subtract the same value.

        3. take the subtracted value out of the bracket (multiply by a).
            f(x) = -4(x^2 + 8x + 16) + 6 + 64

        4. now we can factor the bracket into a binomial.
            f(x) = -4(x + 4)^2 + 70

        From this final equation we can get the vertex which is (-4, 70)
        To only get the vertex, we don't need to go through all of the steps.
        We only need half of the second coefficient for the x of the vertex.
        The y of the vertex is half of the second coefficient squared times a, subtracted from c.
        """
        x = (self.b / self.a) / 2
        x = -x if not x == 0 else 0
        y = self.c - (x ** 2 * self.a)
        return round(x, 3), round(y, 3)

    def vertex_form(self):
        """
        This function gets the vertex and goes the extra step of printing
        the function in proper vertex form.
        """
        if self.b == 0 and self.c == 0:
            return u'f(x) = %dx\u00b2'.encode('UTF-8') % self.a
        x = (self.b / self.a) / 2
        y = self.c - (x ** 2 * self.a)
        return u'f(x) = %.2f(x%+.2f)\u00b2%+.2f'.encode('UTF-8') % (self.a, round(x, 3), round(y, 3))

    def evaluate(self, x):
        """
        Returns y value for any given x
        """
        return self.a * (x**2) + self.b * x + self.c

    def graph(self, x_range=None, vector=False):
        """
        This function uses matplotlib to graph the function.
        If no range is given, it takes the x of the vertex and
        graphs in a range of 5 on both sides.

        I'm using numpy linspace to create 1000 evenly spaced points within the range.
        So the graph is always smooth no matter how small the range.

        The function loops through the array of x and simply plugs it into the function
        and plots each point.

        It also adds the equation, the x and y intercepts and the vertex as labels.

        It saves the graph as "graph.png" into the same directory by default.
        If the vector option is on, it saves as a vector image "graph.pdf".
        """
        vertex = self.vertex()
        if x_range:
            x = ls(x_range[0], x_range[1], 1000)
        else:
            x = ls(vertex[0] - 5, vertex[0] + 5)
        y = [((self.a * (i ** 2)) + (self.b * i) + self.c) for i in x]
        plt.plot(x, y)
        x_ints = self.x_intercepts() if self.has_x_intercept() else None

        patches = [mpatch.Patch(color='black', label=self.__repr__(False)),
                   mpatch.Patch(color='red', label="Vertex " + str(vertex)),
                   mpatch.Patch(color='blue', label="Y Intercept " + str(self.y_int))]
        if x_ints is not None:
            patches.append(mpatch.Patch(color='orange', label="X Intercept(s) " + str(x_ints)))

        plt.grid()
        plt.legend(handles=patches)
        if vector:
            plt.savefig('graph.pdf', bbox_inches='tight')
        else:
            plt.savefig('graph.png', bbox_inches='tight')


import numpy as np

class BinomialTree:
  def __init__(self, S0, r, d, sigma, T):
    self.S0 = S0
    self.r = r
    self.d = d
    self.sigma = sigma
    self.T = T

  def generate_tree(self, N, model = 'crr'):
    '''
    generate an upper triangular matrix recording all possible price paths
    by using up and down moves only S0, sigma, T and N.

    inputs:
    N: number of levels of node
    mode: up and down multipliers are decided by either crr or GBM model

    Outputs:
    dt: T / N
    up: up-move multiplier
    down: down-move multiplier
    price_tree: numpy array containing prices

    '''

    dt = self.T / N

    # Construct a price tree using numpy array
    # Given N periods, there are (N+1) nodes in total
    # Period starts from 0 (current price)
    price_tree = np.zeros((N+1,N+1))

    if model == 'crr':
      # Cox, Ross & Rubinstein (CRR) formulas are
      # $$up = e^{\sigma\sqrt{\Delta t}} \quad \text{and} \quad down = e^{-\sigma\sqrt{\Delta  t}} =\frac{1}{up}.$$
      # Note that ud = 1
      up = np.exp(self.sigma * np.sqrt(dt))
      down = 1 / up

    elif model == 'gbm':
      # Geometric Brownian Motion (GBM) formulas are
      # $$up = e^{ (r - d - 0.5 \sigma^2) \Delta t + \sigma\sqrt{\Delta t}} \quad \text{and} \quad down = e^{ (r - d - 0.5 \sigma^2) \Delta t - \sigma\sqrt{\Delta t}}.$$
      # Note that ud \neq 1
      # Simplifying the discrete GBM gives
      # S_{j\Delta t} = S_{(j-1)\Delta t} exp( ( r - 0.5 \sigma^2)\Delta t + \sigma \sqrt{\Delta t}Z_j)

      det_comp = (self.r - self.d - 0.5 * self.sigma ** 2) * dt
      stoc_comp = self.sigma * np.sqrt(dt)

      up = np.exp(det_comp + stoc_comp)
      down = np.exp(det_comp - stoc_comp)

    # concepts: at [i,j]-th entry of the tree,
    # number of up + number of down = j (col)
    # number of down = i (row)
    # So, number of up = j - i
    # Hence, [i,j]-th entry = S0 x up^{j-i} x down^i

    size = (N+1, N+1)
    f = lambda i,j : up**(j-i) * down**i if i <= j else 0
    price_tree = self.S0 * np.fromfunction(np.vectorize(f), size)

    return dt, up, down, price_tree

  def option_price(self, num_sim, payoff, model = 'crr', exercise_style = 'European'):
    '''
    inputs:
    num_sim: number of simulations
    payoff: (lambda function) option's payoff function at maturity
    model: either crr or gbm
    exercise_style: either European or American

    output:
    list of (should converge) prices
    '''
    prices = [0] * num_sim

    for a in range(1, num_sim + 1):
      N = 2 ** a
      dt, up, down, price_tree = self.generate_tree(N, model = model)

      # price option
      if model == 'crr':
        # To ensure no-arbitrage, we must have
        # 0 <= p_tilde <= 1, that is, d <= e^{r\delta t} <= u, that is,
        # e^{-sigma \sqrt{\Delta t}} <= e^{r\delta t} <= e^{sigma \sqrt{\Delta t}}, that is,
        # -sigma <= r \sqrt{T/N} <= sigma
        # Since the LHS is always true, so we just need to check r \sqrt{T/N} <= sigma
        assert self.r * np.sqrt(self.T/N) <= self.sigma, ' r \sqrt{T/N} <= sigma is not fulfilled, leading to arbitrage'

      p_tilde = (np.exp( (self.r - self.d) * dt ) - down) / (up - down)
      cash_flows = np.zeros((N+1,N+1))
      cash_flows[:, -1] =  payoff(price_tree[:, -1])

      if exercise_style == 'European':
        for j in range(N-1, -1, -1):
          for i in range(j+1):
            cash_flows[i,j] = np.exp(-self.r * dt) * (p_tilde * cash_flows[i,j+1] + (1-p_tilde) * cash_flows[i+1,j+1] )

      elif exercise_style == 'American':
        exercise_value = payoff(price_tree)
        for j in range(N-1, -1, -1):
          for i in range(j+1):
            cash_flows[i,j] = np.maximum(exercise_value[i,j], np.exp(-self.r * dt) * (p_tilde * cash_flows[i,j+1] + (1-p_tilde) * cash_flows[i+1,j+1] ))

      prices[a - 1] = cash_flows[0,0]

    return prices


#!/usr/local/bin/python3.9
'''
Module for significance testing

Change Log
==========
0.0.1 (2021-04-11)
0.0.2 (2021-07-07)
----------
implemented loop option for multiple variables in dataframe
'''
import numpy as np
from scipy import stats

np.random.seed(7654567)  # fix seed to get the same result

'''
One-sample t-test
two-sided test for the null hypothesis that the expected value (mean) of a
sample of independent observations a is equal to the given population mean, popmean
'''
rvs = stats.norm.rvs(loc=5, scale=10, size=(50))
stat, p = stats.ttest_1samp(rvs,5.0)
print(stat)
print(p)

'''
Independent-samples t-test
two-sided test for the null hypothesis that 2 independent samples have identical average
(expected) values. This test assumes that the populations have identical variances by default.

Perform check for homogeneity of variance prior to running
i.e., Levene Test
'''
rvs1 = stats.norm.rvs(loc=5,scale=10,size=500)
rvs2 = stats.norm.rvs(loc=5,scale=10,size=500)
rvs3 = stats.norm.rvs(loc=5,scale=30,size=500)

#Equal variance
from scipy.stats import levene
levene(rvs1, rvs2)

stat, p = stats.ttest_ind(rvs1, rvs2, equal_var=True, alternative='two-sided')
print(stat, p)

# Unequal variance
levene(rvs1, rvs3)

stat, p = stats.ttest_ind(rvs1, rvs3, equal_var=False)
print(stat, p)

# Mann-Whitney U-test
n = 10000
start = 0
width = 20

a = 0
data_normal = skewnorm.rvs(size=n, a=a,loc = start, scale=width)

a = 3
data_skew = skewnorm.rvs(size=n, a=a,loc = start, scale=width)

f, (ax1, ax2) = plt.subplots(1, 2)
ax1.hist(data_normal, bins='auto')
ax1.set_title('probability density (random)')
ax2.hist(data_skew, bins='auto')
ax2.set_title('Skewed data')
plt.tight_layout()

stat, p = stats.mannwhitneyu(data_normal, data_skew, alternative='two-sided')
print(stat)
print('%.5f' % p)
results = stats.mannwhitneyu(data_normal, data_skew, alternative='two-sided')

'''
Paired samples t-test
two-sided test for the null hypothesis that 2 related
or repeated samples have identical average (expected) values
'''
np.random.seed(12345678)
rvs1 = stats.norm.rvs(loc=5,scale=10,size=500)
rvs2 = (stats.norm.rvs(loc=5,scale=10,size=500) + stats.norm.rvs(scale=0.2,size=500))
stats.ttest_rel(rvs1,rvs2, alternative='two-sided')

'''
Wilcox signed-rank test
Tests the null hypothesis that two related paired samples come from the same distribution.
In particular, it tests whether the distribution of the differences x - y is symmetric about zero.
It is a non-parametric version of the paired T-test
'''
from numpy.random import randn
from scipy.stats import wilcoxon
data1 = 5 * randn(100) + 50
data2 = 5 * randn(100) + 51
stat, p = wilcoxon(data1, data2)
print('Statistic=%.3f, p=%.3f' % (stat, p))

# Code for interpretation
alpha = 0.05
if p > alpha:
	print('Fail to reject H0')
else:
	print('Reject H0')

'''
Loop through columns in dataframe
Interpret Hypothesis
FDR correction for multiple p-values

2 options:
1. parametric (normallly distributed) variables - run t-test
2. non-parametric (not normally distributed variables) - run Wilcox test
'''
# One-sample t-test (for parametric data)
col_names_par = ['', '', '']
true_mu = 0

pvals_par = []
for i in col_names_par:
	print('\n')
	w, p = stats.ttest_1samp(df[i], true_mu)
	print(i)
	print(w, p)
	pvals_par.append(p)
	alpha = 0.05
	if p > alpha:
		print('Fail to reject H0\nNot statistically different from zero')
	else:
		print('Reject H0\n*****Statistically different from zero*****')

# FDR correction for parametric p-values
multi.multipletests(pvals_par, alpha=0.05, method='fdr_bh',
					is_sorted=False, returnsorted=False)

# Wilcox test (for non-parametric data)
# col_names_npar
col_names_npar = ['', '', '']

pvals_npar = []
for i in col_names_npar:
	print('\n')
	w, p = wilcoxon(df[i])
	print(i)
	print(w, p)
	pvals_npar.append(p)
	alpha = 0.05
	if p > alpha:
		print('Fail to reject H0\nNot statistically different from zero')
	else:
		print('Reject H0\n*****Statistically different from zero*****')

# FDR correction for non-parametric p-values
multi.multipletests(pvals_npar, alpha=0.05, method='fdr_bh',
					is_sorted=False, returnsorted=False)


# coding: utf-8

__author__ = 'Mário Antunes'
__version__ = '0.1'
__email__ = 'mariolpantunes@gmail.com'
__status__ = 'Development'

import math
import numpy as np

def lagrange_derivative(x: float, x0: float, x1: float, x2: float, y0: float, y1: float, y2: float) -> float:
    """
    Three point lagrange derivative.

    It computes the derivative of \\(x\\) based on three points:
    \\([(x_0, y_0), (x_1, y_1), (x_2, y_2)]\\)
    Depending od the value of \\(x\\), we can compute the forward,
    backward or central derivative.

    Args:
        x (float): the value where the derivative will be computed
        x0 (float): first x value
        x1 (float): second x value
        x2 (float): third x value
        y0 (float): first y value
        y1 (float): second y value
        y2 (float): third y value

    Returns:
        float: the first derivative for \\(x\\) value

    """
    p0 = y0 * (2*x-x1-x2) / ((x0-x1)*(x0-x2))
    p1 = y1 * (2*x-x0-x2) / ((x1-x0)*(x1-x2))
    p2 = y2 * (2*x-x0-x1) / ((x2-x0)*(x2-x1))
    return p0+p1+p2

def cfd(x: np.ndarray, y: np.ndarray) -> np.ndarray:
    """
    Computes the central first order derivative for uneven space sequences.

    This method uses the three point lagrange derivative to compute the
    derivative in uneven time series.
    The first and last elements are computed beased on the forward and
    backward definition of the first derivative.

    Args:
        x (np.ndarray): the value of the points in the x axis coordinates
        y (np.ndarray): the value of the points in the y axis coordinates

    Returns:
        np.ndarray: the first order derivative
    """
    d1 = []

    # compute the first point with the forward definition
    y0, y1, y2 = y[0:3]
    x0, x1, x2 = x[0:3]
    d = lagrange_derivative(x0, x0, x1, x2, y0, y1, y2)
    d1.append(d)

    # compute n-2 points with the central definition
    for i in range(1, len(x) - 1):
        y0, y1, y2 = y[i-1:i+2]
        x0, x1, x2 = x[i-1:i+2]
        d = lagrange_derivative(x1, x0, x1, x2, y0, y1, y2)
        d1.append(d)

    # compute the last point with the backwards definition
    y0, y1, y2 = y[-3:len(y)+1]
    x0, x1, x2 = x[-3:len(x)+1]
    d = lagrange_derivative(x2, x0, x1, x2, y0, y1, y2)
    d1.append(d)

    return np.array(d1)

def csd(x: np.ndarray, y: np.ndarray) -> np.ndarray:
    """
    Computes the central second order derivative for uneven space sequences.

    The computation is based on the second order polynomial fitting.

    Args:
        x (np.ndarray): the value of the points in the x axis coordinates
        y (np.ndarray): the value of the points in the y axis coordinates

    Returns:
        np.ndarray: the second order derivative
    """
    d2 = []

    # compute the first point with the forward definition
    y0, y1, y2 = y[0:3]
    x0, x1, x2 = x[0:3]
    d = 2.0 * ((x1-x0)*y2 - (x2-x0)*y1 + (x2-x1)*y0) / ((x1-x0)*(x2-x1)*(x2-x0))
    d2.append(d)

    # compute n-2 points with the central definition
    for i in range(1, len(x) - 1):
        y1, y2, y3 = y[i-1:i+2]
        x1, x2, x3 = x[i-1:i+2]
        d = (2*y1/((x2-x1)*(x3-x1))) - (2*y2/((x3-x2)*(x2-x1)))+(2*y3/((x3-x2)*(x3-x1)))
        d2.append(d)

    # compute the last point with the backwards definition
    y2, y1, y0 = y[-3:len(y)+1]
    x2, x1, x0 = x[-3:len(x)+1]
    d = 2.0 * ((x1-x2)*y0 - (x0-x2)*y1 + (x0-x1)*y2) / ((x1-x2)*(x0-x1)*(x0-x2))
    d2.append(d)

    return np.array(d2)


"""
[2015-05-11] Challenge #214 [Easy] Calculating the standard deviation
http://www.reddit.com/r/dailyprogrammer/comments/35l5eo/20150511_challenge_214_easy_calculating_the/

Description

Standard deviation is one of the most basic measurments in statistics. For some collection of values (known as a "population" in statistics), it measures how dispersed those values are. If the standard deviation is high, it means that the values in the population are very spread out; if it's low, it means that the values are tightly clustered around the mean value.
For today's challenge, you will get a list of numbers as input which will serve as your statistical population, and you are then going to calculate the standard deviation of that population. There are statistical packages for many programming languages that can do this for you, but you are highly encouraged not to use them: the spirit of today's challenge is to implement the standard deviation function yourself.
The following steps describe how to calculate standard deviation for a collection of numbers. For this example, we will use the following values:
5 6 11 13 19 20 25 26 28 37
First, calculate the average (or mean) of all your values, which is defined as the sum of all the values divided by the total number of values in the population. For our example, the sum of the values is 190 and since there are 10 different values, the mean value is 190/10 = 19
Next, for each value in the population, calculate the difference between it and the mean value, and square that difference. So, in our example, the first value is 5 and the mean 19, so you calculate (5 - 19)2 which is equal to 196. For the second value (which is 6), you calculate (6 - 19)2 which is equal to 169, and so on.
Calculate the sum of all the values from the previous step. For our example, it will be equal to 196 + 169 + 64 + ... = 956.
Divide that sum by the number of values in your population. The result is known as the variance of the population, and is equal to the square of the standard deviation. For our example, the number of values in the population is 10, so the variance is equal to 956/10 = 95.6.
Finally, to get standard deviation, take the square root of the variance. For our example, sqrt(95.6) ≈ 9.7775.
Formal inputs & outputs

Input

The input will consist of a single line of numbers separated by spaces. The numbers will all be positive integers.
Output

Your output should consist of a single line with the standard deviation rounded off to at most 4 digits after the decimal point.
Sample inputs & outputs

Input 1

5 6 11 13 19 20 25 26 28 37
Output 1

9.7775
Input 2

37 81 86 91 97 108 109 112 112 114 115 117 121 123 141
Output 2

23.2908
Challenge inputs

Challenge input 1

266 344 375 399 409 433 436 440 449 476 502 504 530 584 587
Challenge input 2

809 816 833 849 851 961 976 1009 1069 1125 1161 1172 1178 1187 1208 1215 1229 1241 1260 1373
Notes

For you statistics nerds out there, note that this is the population standard deviation, not the sample standard deviation. We are, after all, given the entire population and not just a sample.
If you have a suggestion for a future problem, head on over to /r/dailyprogrammer_ideas and let us know about it!
"""

# 1st implementation: clear, short, matches the problem definition
def standard_deviation_1(input_str):
    list = [int(s) for s in input_str.split()]
    count = len(list)
    avg = sum(list) / count
    variance = sum((n - avg)**2 for n in list) / count
    return variance**0.5

# 2nd implementation: 1 pass, O(1) memory usage
def standard_deviation(input_str):
    n_sum, n2_sum, count = 0, 0, 0
    for s in input_str.split():
        n = int(s)
        n_sum += n
        n2_sum += n*n
        count += 1
    if count == 0:
        return None
    avg = n_sum / count
    return (n2_sum/count - n_sum*n_sum/(count*count))**0.5

def tests():
    assert math.trunc(standard_deviation('5 6 11 13 19 20 25 26 28 37') * 10000.0) == 97775, 'Test 1'
    assert math.trunc(standard_deviation('37 81 86 91 97 108 109 112 112 114 115 117 121 123 141') * 10000.0) == 232908, 'Test 2'
    assert math.trunc(standard_deviation('266 344 375 399 409 433 436 440 449 476 502 504 530 584 587') * 10000.0) == 836615, 'Test 3'
    assert math.trunc(standard_deviation('809 816 833 849 851 961 976 1009 1069 1125 1161 1172 1178 1187 1208 1215 1229 1241 1260 1373') * 10000.0) == 1701272, 'Test 4'
    print('All tests passed')

if __name__ == '__main__':
    tests()

# Python 3.4 => import statistics; statistics.pstdev(list)
# Others => import numpy; numpy.std()


"""Use Newton's method to solve systems of nonlinear algebraic equations."""
import numpy as np

def Newton_system(F, J, x, eps):
    """
    Solve nonlinear system F=0 by Newton's method.
    J is the Jacobian of F. Both F and J must be functions of x.
    At input, x holds the start value. The iteration continues
    until ||F|| < eps.
    """
    F_value = F(x)
    F_norm = np.linalg.norm(F_value, ord=2)     # l2 norm of vector
    iteration_counter = 0
    while abs(F_norm) > eps and iteration_counter < 100:
        delta = np.linalg.solve(J(x), -F_value)
        x = x + delta
        F_value = F(x)
        F_norm = np.linalg.norm(F_value, ord=2)
        iteration_counter = iteration_counter + 1

    # Here, either a solution is found, or too many iterations
    if abs(F_norm) > eps:
        iteration_counter = -1
    return x, iteration_counter

def test_Newton_system1():
    from numpy import cos, sin, pi, exp

    def F(x):
        return np.array(
            [x[0]**2 - x[1] + x[0]*cos(pi*x[0]),
             x[0]*x[1] + exp(-x[1]) - x[0]**(-1.)])

    def J(x):
        return np.array(
            [[2*x[0] + cos(pi*x[0]) - pi*x[0]*sin(pi*x[0]), -1],
             [x[1] + x[0]**(-2.), x[0] - exp(-x[1])]])

    expected = np.array([1, 0])
    tol = 1e-4
    x, n = Newton_system(F, J, x=np.array([2, -1]), eps=0.0001)
    print(n, x)
    error_norm = np.linalg.norm(expected - x, ord=2)
    assert error_norm < tol, 'norm of error ={:g}'.format(error_norm)
    print('norm of error ={:g}'.format(error_norm))

def test_Newton_system2():

    def F(x):
        return np.array(
            [x[0]**2 - x[1] + x[0] - 2,
             x[1]*x[0] + x[1]**2 + x[0] - 1])

    def J(x):
        return np.array(
            [[2*x[0] + 1, -1],
             [x[1] + 1, x[0] + 2*x[1]]])

    expected = np.array([1, 0])
    tol = 1e-4
    x, n = Newton_system(F, J, x=np.array([2, -0.5]), eps=0.0001)
    print(n, x)
    error_norm = np.linalg.norm(expected - x, ord=2)
    assert error_norm < tol, 'norm of error ={:g}'.format(error_norm)
    print('norm of error ={:g}'.format(error_norm))

if __name__ == '__main__':
    test_Newton_system1()
    test_Newton_system2()


import numpy as np
from scipy.stats import beta
from matplotlib import pyplot as plt

visitors_to_A = 1300
visitors_to_B = 1275
conversions_from_A = 120
conversions_from_B = 125

alpha_prior = 1
beta_prior = 1

posterior_A = beta(alpha_prior + conversions_from_A,
                   beta_prior + visitors_to_A - conversions_from_A)
posterior_B = beta(alpha_prior + conversions_from_B,
                   beta_prior + visitors_to_B - conversions_from_B)

samples = 20000 # We want this to be large to get a better approximation.

samples_posterior_A = posterior_A.rvs(samples)
samples_posterior_B = posterior_B.rvs(samples)

print((samples_posterior_A > samples_posterior_B).mean())

x = np.linspace(0,1, 500)
plt.plot(x, posterior_A.pdf(x), label='posterior of A')
plt.plot(x, posterior_B.pdf(x), label='posterior of B')
plt.xlabel('Value')
plt.ylabel('Density')
plt.title("Posterior distributions of the conversion rates of Web pages $A$ and $B$")
plt.legend()
plt.show()

plt.plot(x, posterior_A.pdf(x), label='posterior of A')
plt.plot(x, posterior_B.pdf(x), label='posterior of B')
plt.xlim(0.05, 0.15)
plt.xlabel('Value')
plt.ylabel('Density')
plt.title("Zoomed-in posterior distributions of the conversion rates of Web pages $A$ and $B$")
plt.legend()

plt.show()


import numpy as np
import xlsxwriter as xl
import time

# Função utilizada: f(x) = 3 * cos(x) - e^x/3 // Intervalo: [-5; 2]// h: 0.75 // Precisão: 10^-5
# f'(x) = -3sen(x) - 1/3 * e^x

tempo_inicial = time.time()

# Utilizando o método de Newton para o intervalo [1; 1.75]:

# Xo é um chute inicial de qualquer valor entre o intervalo I.

def test(x):
    resultado = 3 * np.cos(x) - (np.exp(x)/3)
    return resultado

def derivada(x):
    resultado = - 3 * np.sin(x) - (1/3) * (np.exp(x)/3)
    return resultado

def newton():
    c = 0
    x_barra = [1.375]  # O Xo é um chute inicial de qualquer valor no I. Nesse caso, o valor do meio
    x_aplicado = []
    while True:
        x_aplicado_atual = test(x_barra[-1])
        if x_aplicado_atual < 0:
            modulo_x_aplicado_atual = x_aplicado_atual * (-1)
        else:
            modulo_x_aplicado_atual = x_aplicado_atual
        if modulo_x_aplicado_atual < 10 ** -5:
            x_aplicado.append(x_aplicado_atual)
            break
        else:
            derivada_x_barra = derivada(x_barra[-1])
            x_aplicado.append(x_aplicado_atual)
            x_barra_atual = x_barra[-1] - (x_aplicado[-1] / derivada_x_barra)
            x_barra.append(x_barra_atual)
            c += 1
    return x_barra[-1], x_barra, x_aplicado, c

raiz_aproximada = newton()[0]
lista_x_barra = newton()[1]
lista_x_aplicado = newton()[2]
iteracoes = newton()[3]

print('Questao teste, intervalo [1; 1.75]:')
print(f'A raiz aproximada é: {raiz_aproximada}')
print(f'Para encontrar essa raiz, foram necessárias {iteracoes} iterações.')
print(lista_x_barra)
print(lista_x_aplicado)

"""# Criando a tabela com os valores obtidos
# Criando o arquivo
outWorkbook = xl.Workbook("Questao_03_3o_intervalo_newton.xlsx")
outSheet = outWorkbook.add_worksheet()

# Declarando os títulos

# Escrevendo os dados no arquivo
outSheet.write("A1", "x_barra")
outSheet.write("B1", "f(x_barra)")

for number, item in enumerate(lista_x_barra):
    outSheet.write(number + 1, 0, item)
for number, item in enumerate(lista_x_aplicado):
    outSheet.write(number + 1, 1, item)
outWorkbook.close()"""

tempo_final = time.time()
tempo_gasto = tempo_final - tempo_inicial
print(f'Foram gastos {tempo_gasto}s.')


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

def circle_2D():
	# CIRCLE APPROXIMATION FUNCTION

	plt.figure(1,figsize=(13,12))
	for count in range(1,8):
		hit=0
		N=10**count
		plt.subplot(3,3,count)
		randomx = np.random.uniform(-1,1,size=N)
		randomy = np.random.uniform(-1,1,size=N)
		plt.axis([-1,1,-1,1])
		powersum = np.power(randomx,2)+np.power(randomy,2) #condition for any point to be inside the circle
		hit=np.count_nonzero(powersum<=1) #Number of points inside the circle
		pi = 4.0*hit/N #Estimating pi by equating area probabilty with empirical probabilty
		print('for n = %i, approx pi = %f' %(N,pi))

		#Points inside the circle
		circlex = randomx[np.nonzero(powersum<=1)]
		circley = randomy[np.nonzero(powersum<=1)]

		#Points outside the circle
		outx = randomx[np.nonzero(powersum>1)]
		outy = randomy[np.nonzero(powersum>1)]

		plt.plot(circlex,circley,'bo')
		plt.plot(outx,outy,'ro')

		plt.title(r'N: $10^%i$ pi : %f' %(count,pi))

	#plt.show()

def sphere_3D():
	#SPHERE APPROXIMATION FUNCTION

	for count in range(1,8):
		plt.figure(count+1)
		hit=0
		N=10**count
		ax=plt.axes(projection='3d')
		randomx = np.random.uniform(-1,1,size=N)
		randomy = np.random.uniform(-1,1,size=N)
		randomz = np.random.uniform(-1,1,size=N)

		powersum = np.power(randomx,2)+np.power(randomy,2)+np.power(randomz,2) #condition for any point to be inside the sphere
		hit=np.count_nonzero(powersum<=1) #Number of points inside the sphere
		pi = 6.0*hit/N #Estimating pi by equating volume probabilty with empirical probabilty
		print('for n = %i, approx pi = %f' %(N,pi))

		#Points inside the sphere
		spherex = randomx[np.nonzero(powersum<=1)]
		spherey = randomy[np.nonzero(powersum<=1)]
		spherez = randomz[np.nonzero(powersum<=1)]

		#Points outside the sphere
		outx = randomx[np.nonzero(powersum>1)]
		outy = randomy[np.nonzero(powersum>1)]
		outz = randomz[np.nonzero(powersum>1)]
		ax.scatter3D(spherex,spherey,spherez,c='b',marker='o')
		ax.scatter3D(outx,outy,outz,c='r',marker='o')

		plt.title(r'N: $10^%i$ pi : %f' %(count,pi))

	plt.show()

if __name__=='__main__':
	# MAIN FUNCTION

	print('\n')
	print('Printing pi values for Part A: Circle (2D)')
	circle_2D()

	print('\n')
	print('Printing pi values for Part B: Sphere (3D)')
	sphere_3D()


import numpy as np

def euclidian_distance(a, b):
    """
    Compute Euclidian distance between two 1-D arrays.

    Parameters
    ----------
    a: array_like
        Input array.
    b: array_like
        Input array.

    Returns
    -------
    Euclidian distance between two vectors.
    """
    distance = 0.0

    for i in range(len(a)):
        distance += (a[i] - b[i])**2

    return np.sqrt(distance)


import scipy
import scipy.special
import scipy.integrate
from numpy import exp,cos,sin

# A basic 1D integral:
scipy.integrate.quad(exp, 0, 1)
# (1.7182818284590453, 1.9076760487502457e-14)
scipy.integrate.quad(sin, -0.5, 0.5)
# (0.0, 2.707864644566304e-15)
scipy.integrate.quad(cos, -0.5, 0.5)
# (0.9588510772084061, 1.0645385431034061e-14)

f = lambda x : exp(-x**2)
scipy.integrate.quad(f, 0, 1)
# (0.7468241328124271, 8.291413475940725e-15)

scipy.integrate.quad(lambda x : exp(-x**2), 0, 1)
# (0.7468241328124271, 8.291413475940725e-15)

scipy.integrate.quad(lambda x : exp(-x**2), 0, inf)
# (0.8862269254527579, 7.101318390472462e-09)

scipy.integrate.quad(lambda x : exp(-x**2), -inf, 1)
# (1.6330510582651852, 3.669607414547701e-11)

scipy.integrate.quad(lambda x: scipy.special.jn(1,x),0,5)
# (1.177596771314338, 1.8083362065765924e-14)

#### Integrating Polynomials

p = np.poly1d([2, 5, 1])
p(1), p(2), p(3.5)

P = polyint(p)
q=P(5)-P(1)

#### Basic computations in linear algebra
import scipy.linalg

a = array([[-2, 3], [4, 5]])
scipy.linalg.det(a)

b = scipy.linalg.inv(a)
dot(a,b)

#### Solving systems of linear equations¶
import scipy.linalg

A = array([[2, 4, 6], [1, -3, -9], [8, 5, -7]])
b = array([4, -11, 2])

sol1 = scipy.linalg.solve(A,b)

Ainv = scipy.linalg.inv(A)
sol2 = dot(Ainv, b)
sol1==sol2


import numpy as np

def gaussianKernel(x1, x2, sigma):
    """returns a gaussian kernel between x1 and x2
    and returns the value in sim
    """

# Ensure that x1 and x2 are column vectors
#     x1 = x1.ravel()
#     x2 = x2.ravel()

# You need to return the following variables correctly.
    sim = 0

# ====================== YOUR CODE HERE ======================
# Instructions: Fill in this function to return the similarity between x1
#               and x2 computed using a Gaussian kernel with bandwidth
#               sigma
#
#
    sim = np.exp(-np.sum(np.power((x1 - x2), 2)) / float(2 * (sigma**2)))
    return sim
# =============================================================


"""
Unsupervised learning
=====================
A category of learning algorithms is able to discover inherent groups that may exist in a set of data.

K-means algorithm
-----------------
The k-means algorithm uses the mean points in a given dataset to cluster and discover groups within the dataset.
K is the number of clusters that we want and are hoping to discover. After the k-means algorithm has generated the
groupings, we can pass it additional but unknown data for it to predict to which group it will belong.

Note that in this kind of algorithm, only the raw uncategorized data is fed to the algorithm.
It is up to the algorithm to find out if the data has inherent groups within it.

E.g. 100 data points consisting of x and y values. We will feed these values to the learning algorithm and expect that
the algorithm will cluster the data into two sets. We will color the two sets so that the clusters are visible.

Each data point in original_set will belong to a cluster after our k-means algorithm has finished its training.
The k-mean algorithm represents the two clusters it discovers as 1s and 0s. If we had asked the algorithm to cluster
the data into four, the internal representation of these clusters would have been 0, 1, 2, and 3.

The algorithm discovers two distinct clusters in our sample data.
The two mean points of the two clusters are denoted with the red star symbol.

Prediction
---------
With the two clusters that we have obtained, we can predict the group that a new set of data might belong to.

At the barest minimum, we can expect the two test datasets to belong to different clusters. Prove: right when the print
statement prints 1 and 0, thus confirming that our test data does indeed fall under two different clusters.
"""

# K-means algorithm

import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans

# Let's create a sample data of 100 records of x and y pairs
original_set = -2 * np.random.rand(100, 2)
second_set = 1 + 2 * np.random.rand(50, 2)
# The last 50 numbers in original_set will be replaced
original_set[50:100, :] = second_set
# =>  we created two subsets of data, one set has numbers in the negative while the other in the positive.
# print(original_set)
# print(second_set)

# makes the algorithm cluster all its data under only two groups.
k_mean = KMeans(n_clusters=2)
k_mean.fit(original_set)

# The clusters generated by the algorithm will revolve around a certain mean point.
# => get the points that define these two mean points:
print(k_mean.cluster_centers_)
# to print out the various clusters that each dataset belongs:
print(k_mean.labels_)

# There are 100 1s and 0s. Each shows the cluster that each data point falls under.
# To chart the points of each group and color it appropriately to show the clusters:
for i in set(k_mean.labels_):
    # select all points that correspond to the group i
    # When i=0, all points belonging to the group 0 are returned to index
    index = k_mean.labels_ == i
    # plots these data points using o as the character for drawing each point.
    plt.plot(original_set[index, 0], original_set[index, 1], 'o')

# plot the centroids or mean values around which the clusters have formed:
plt.plot(k_mean.cluster_centers_[0][0], k_mean.cluster_centers_[0][1], '*', c='r', ms=10)
plt.plot(k_mean.cluster_centers_[1][0], k_mean.cluster_centers_[1][1], '*', c='r', ms=10)

# show the whole graph with the two means illustrated by a star:
plt.show()

# Prediction
# With the two clusters that we have obtained, we can predict the group that a new set of data might belong to.
sample = np.array([[-1.4, -1.4]])
print(k_mean.predict(sample))

another_sample = np.array(([[2.5, 2.5]]))
print(k_mean.predict(another_sample))


import numpy as np

def l21shrink(epsilon, x):
    output = x.copy()
    norm = np.linalg.norm(x, ord=2, axis=0)
    for i in range(x.shape[1]):
        if norm[i] > epsilon:
            for j in range(x.shape[0]):
                output[j,i] = x[j,i] - epsilon * x[j,i] / norm[i]
        elif norm[i] < -epsilon:
            for j in range(x.shape[0]):
                output[j,i] = x[j,i] + epsilon * x[j,i] / norm[i]
        else:
            output[:,i] = 0.
    return output


# %%

from typing import Sequence
from numba import njit
# %%

def fib(n: int):
    if(n <= 2):
        return 1
    return fib(n-1) + fib(n-2)

# print(fib(3))
# print(fib(5))
# print(fib(8))

def fib_memo(n: int, memo={1: 1, 2: 1}):
    if n >= 1:
        if (n in memo):
            return memo[n]
        memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
        return memo[n]

    else:
        print("Provide Positive int")

# %%

# print(fib_memo(100))

# @njit(fastmath=True, cache=True)
def grid_traveller(x: int, y: int, memo={}) -> int:
    if (x, y) in memo.keys():
        return memo[(x, y)]
    if x*y == 0:
        return 0
    if x*y == 1:
        return 1

    res = grid_traveller(x-1, y, memo) + grid_traveller(x, y-1, memo)
    memo[(x, y)] = res
    return res

print(grid_traveller(1, 1))
print(grid_traveller(3, 3))
print(grid_traveller(10, 10))

# %%

def can_sum(target: int, arr: Sequence[int]) -> bool:
    pass
# %%

def how_sum(target: int, arr: Sequence[int]) -> Sequence[int]:
    pass
# %%

def can_sum(target: int, arr: Sequence[int]):
    pass


import numpy
from matplotlib import pyplot

def euler(f, x_end, y0, N):
    x, dx = numpy.linspace(0, x_end, N+1, retstep=True)
    y = numpy.zeros((len(y0),N+1))
    y[:,0] = y0
    for n in range(N):
        y[:,n+1] = y[:,n] + dx * f(x[n], y[:,n])
    return x, dx, y

def euler_pc(f, x_end, y0, N):
    x, dx = numpy.linspace(0, x_end, N+1, retstep=True)
    y = numpy.zeros((len(y0),N+1))
    y[:,0] = y0
    for n in range(N):
        fn = f(x[n], y[:,n])
        yp = y[:,n] + dx * fn
        y[:,n+1] = y[:,n] + dx / 2 * (fn + f(x[n+1], yp))
    return x, dx, y

def rk4(f, x_end, y0, N):
    x, dx = numpy.linspace(0, x_end, N+1, retstep=True)
    y = numpy.zeros((len(y0),N+1))
    y[:,0] = y0
    for n in range(N):
        k1 = dx * f(x[n]         , y[:,n]         )
        k2 = dx * f(x[n] + dx / 2, y[:,n] + k1 / 2)
        k3 = dx * f(x[n] + dx / 2, y[:,n] + k2 / 2)
        k4 = dx * f(x[n] + dx    , y[:,n] + k3    )
        y[:,n+1] = y[:,n] + (k1 + 2 * (k2 + k3) + k4) / 6
    return x, dx, y

if __name__=="__main__":

    def f_sin(x, y):
        return -numpy.sin(x)
    print("Euler Predictor-Corrector")
    x, dx, y = euler_pc(f_sin, 0.5, [1], 5)
    print("dx=", dx, "y(0.5)=", y[0,-1])
    x, dx, y = euler_pc(f_sin, 0.5, [1], 50)
    print("dx=", dx, "y(0.5)=", y[0,-1])
    Npoints = 5*2**numpy.arange(1,10)
    dx_all = 0.5/Npoints
    errors = numpy.zeros_like(dx_all)
    for i, N in enumerate(Npoints):
        x, dx, y = euler_pc(f_sin, 0.5, [1], N)
        errors[i] = abs(y[0,-1] - numpy.cos(0.5))
        dx_all[i] = dx
    pyplot.figure(figsize=(12,6))
    pyplot.loglog(dx_all, errors, 'kx')
    pyplot.loglog(dx_all, errors[0]*(dx_all/dx_all[0])**2, 'b-',
                  label=r"$\propto \Delta x^2$")
    pyplot.legend(loc='upper left')
    pyplot.xlabel(r"$\Delta x$")
    pyplot.ylabel("Error")
    pyplot.show()

    def f_circle(x, y):
        dydx = numpy.zeros_like(y)
        dydx[0] = -y[1]
        dydx[1] = y[0]
        return dydx
    y0 = numpy.array([1, 0])
    x, dx, y = euler_pc(f_circle, 50, y0, 500)
    pyplot.figure(figsize=(8,8))
    pyplot.plot(y[0,:], y[1,:])
    pyplot.show()

    print("RK4")
    x, dx, y = rk4(f_sin, 0.5, [1], 5)
    print("dx=", dx, "y(0.5)=", y[0,-1])
    x, dx, y = rk4(f_sin, 0.5, [1], 50)
    print("dx=", dx, "y(0.5)=", y[0,-1])
    Npoints = 5*2**numpy.arange(1,10)
    dx_all = 0.5/Npoints
    errors = numpy.zeros_like(dx_all)
    for i, N in enumerate(Npoints):
        x, dx, y = rk4(f_sin, 0.5, [1], N)
        errors[i] = abs(y[0,-1] - numpy.cos(0.5))
        dx_all[i] = dx
    pyplot.figure(figsize=(12,6))
    pyplot.loglog(dx_all, errors, 'kx')
    pyplot.loglog(dx_all, errors[0]*(dx_all/dx_all[0])**4, 'b-',
                  label=r"$\propto \Delta x^4$")
    pyplot.legend(loc='upper left')
    pyplot.xlabel(r"$\Delta x$")
    pyplot.ylabel("Error")
    pyplot.show()

    y0 = numpy.array([1, 0])
    x, dx, y = rk4(f_circle, 50, y0, 500)
    pyplot.figure(figsize=(8,8))
    pyplot.plot(y[0,:], y[1,:])
    pyplot.show()