# # Chapter 3: Sampling, Convolution and Discrete Fourier Transform

# Author: Sandipan Dey

###########################################

# ## Problems

import numpy as np
import numpy.fft as fp
from scipy import signal
import scipy.fftpack
from skimage.io import imread
from skimage.color import rgb2gray
from skimage.metrics import peak_signal_noise_ratio
from scipy.ndimage import convolve
import cv2
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D  # noqa: F401 unused import
from mpl_toolkits.axes_grid1 import make_axes_locatable
from matplotlib.ticker import LinearLocator, FormatStrFormatter

def plot_image(im, title):
    plt.imshow(im, cmap='gray')
    plt.axis('off')
    plt.title(title, size=20)

def plot_freq_spectrum(F, title, cmap=plt.cm.gray, show_axis=True, colorbar=False):
    plt.imshow((20*np.log10(0.1 + fp.fftshift(F))).real.astype(int), cmap=cmap)
    if not show_axis:
        plt.axis('off')
    if colorbar:
        plt.colorbar()
    plt.title(title, size=20)

h, w = 100, 100
images = list()
im = np.zeros((h,w))
for x in range(h):
    im[x,:] = np.sin(x)
images.append(im)
im = np.zeros((h,w))
for y in range(w):
    im[:,y] = np.sin(y)
images.append(im)

im = np.zeros((h,w))
for x in range(h):
    for y in range(w):
        im[x,y] = np.sin(x + y)
images.append(im)
im = np.zeros((h,w))
for x in range(h):
    for y in range(w):
        if (x-h/2)**2 + (y-w/2)**2 < 100:
            im[x,y] = np.sin(x + y)
images.append(im)

im = np.zeros((h,w))
for x in range(h):
    for y in range(w):
        if (x-h/2)**2 + (y-w/2)**2 < 25:
            im[x,y] = 1
images.append(im)
im = np.zeros((h,w))
im[h//2 -5:h//2 + 5, w//2 -5:w//2 + 5] = 1
images.append(im)

plt.figure(figsize=(25,10))
i = 1
for im in images:
    plt.subplot(2,6,i), plot_image(im, 'image {}'.format(i))
    plt.subplot(2,6,i+6), plot_freq_spectrum(fp.fft2(im), 'DFT {}'.format(i), show_axis=False)
    i += 1
plt.tight_layout()
plt.show()

im = rgb2gray(imread("images/Img_03_01.jpg"))
h, w = im.shape
F = fp.fft2(im)
F_shifted = fp.fftshift(F)

xs = list(map(int, np.linspace(1, h//5, 10)))
ys = list(map(int, np.linspace(1, w//5, 10)))
plt.figure(figsize=(20,8))
plt.gray()
for i in range(10):
    F_mask = np.zeros((h, w))
    F_mask[h//2-xs[i]:h//2+xs[i]+1, w//2-ys[i]:w//2+ys[i]+1] = 1
    F1 = F_shifted*F_mask
    im_out =  fp.ifft2(fp.ifftshift(F1)).real #np.abs()
    plt.subplot(2,5,i+1), plt.imshow(im_out), plt.axis('off')
    plt.title('{}x{},PSNR={}'.format(2*xs[i]+1, 2*ys[i]+1, round(peak_signal_noise_ratio(im, im_out),2)), size=15)
plt.suptitle('Fourier reconstruction by keeping first few frequency basis vectors', size=25)
plt.show()

# ## 1. Sampling to increase/decrease the resolution of an image

# ### 1.1. Up-sampling an image by using DFT and a Low-pass-filter (LPF)

im = 255*rgb2gray(imread('images/Img_03_01.jpg'))
im1 = np.zeros((2*im.shape[0], 2*im.shape[1]))
print(im.shape, im1.shape)
for i in range(im.shape[0]):
    for j in range(im.shape[1]):
        im1[2*i,2*j] = im[i,j]

kernel = [[0.25, 0.5, 0.25], [0.5, 1, 0.5], [0.25, 0.5, 0.25]]

def pad_with_zeros(vector, pad_width, iaxis, kwargs):
    vector[:pad_width[0]] = 0
    vector[-pad_width[1]:] = 0
    return vector

kernel = np.pad(kernel, (((im1.shape[0]-3)//2,(im1.shape[0]-3)//2+1), ((im1.shape[1]-3)//2,(im1.shape[1]-3)//2+1)), pad_with_zeros)

freq = fp.fft2(im1)
freq_kernel = fp.fft2(fp.ifftshift(kernel))

freq_LPF = freq*freq_kernel # by the Convolution theorem

im2 = fp.ifft2(freq_LPF).real

plt.figure(figsize=(15,10))
plt.gray() # show the filtered result in grayscale
cmap = 'nipy_spectral' #'viridis'
plt.subplot(231), plot_image(im, 'Original Input Image')
plt.subplot(232), plot_image(im1, 'Padded Input Image')
plt.subplot(233), plot_freq_spectrum(freq, 'Original Image Spectrum', cmap=cmap)
plt.subplot(234), plot_freq_spectrum(freq_kernel, 'Image Spectrum of the LPF', cmap=cmap)
plt.subplot(235), plot_freq_spectrum(fp.fft2(im2), 'Image Spectrum after LPF', cmap=cmap)
plt.subplot(236), plot_image(im2.astype(np.uint8), 'Output Image')
plt.show()

# ### 1.2. Down-sampling with Anti-Aliasing using Gaussian Filter

from skimage.filters import gaussian
from skimage import img_as_float
#im = rgb2gray(imread('images/Img_03_03.jpg'))
im = img_as_float(imread('images/Img_03_08.jpg'))
print(im.shape)

im_blurred = gaussian(im, sigma=1.25, multichannel=True)

n = 4 # create and image 16 times smaller in size
h, w = im.shape[0] // n, im.shape[1] // n
im_small = np.zeros((h, w, 3))
for i in range(h):
   for j in range(w):
      im_small[i,j] = im[n*i, n*j]
im_small_aa = np.zeros((h, w, 3))
for i in range(h):
   for j in range(w):
      im_small_aa[i,j] = im_blurred[n*i, n*j]

plt.figure(figsize=(15,15))
plt.imshow(im), plt.title('Original Image', size=15)
plt.show()

plt.figure(figsize=(15,15))
plt.imshow(im_small), plt.title('Resized Image (without Anti-aliasing)', size=15)
plt.show()

plt.figure(figsize=(15,15))
plt.imshow(im_small_aa), plt.title('Resized Image (with Anti-aliasing)', size=15)
plt.show()

# ## 2. Denoising an Image with LPF/Notch filter in the Frequency domain

# ### 2.1 Removing Periodic Noise with Notch Filter

im_noisy = rgb2gray(imread("images/Img_03_23.jpg"))
F_noisy = fp.fft2((im_noisy))
print(F_noisy.shape)

def plot_freq_spectrum(F, title, cmap=plt.cm.gray):
    plt.imshow((20*np.log10(0.1 + fp.fftshift(F))).real.astype(int), cmap=cmap)
    plt.xticks(np.arange(0, im.shape[1], 25))
    plt.yticks(np.arange(0, im.shape[0], 25))
    plt.title(title, size=20)

plt.figure(figsize=(20,10))

plt.subplot(121), plot_image(im_noisy, 'Noisy Input Image')
plt.subplot(122), plot_freq_spectrum(F_noisy, 'Noisy Image Spectrum')

plt.tight_layout()
plt.show()

F_noisy_shifted = fp.fftshift(F_noisy)
F_noisy_shifted[180,210] = F_noisy_shifted[200,190] = 0
im_out =  fp.ifft2(fp.ifftshift(F_noisy_shifted)).real #np.abs()

#print(signaltonoise(im1, axis=None))
plt.figure(figsize=(10,8))
plot_image(im_out, 'Output Image')
plt.show()

# ### 2.2 Removing salt-and-pepper noise using Gaussian LPF with scipy fftpack

from scipy import ndimage
from scipy import fftpack
from skimage.util import random_noise

im = rgb2gray(imread('images/Img_03_02.jpg'))
noisy = random_noise(im, mode='s&p')

im_freq = fftpack.fft2(im)
noisy_freq = fftpack.fft2(noisy)
sigma = 1 #0.1
noisy_smoothed_freq = ndimage.fourier_gaussian(noisy_freq, sigma=sigma)
noisy_smoothed = fftpack.ifft2(noisy_smoothed_freq)

fig, ((ax1, ax2), (ax3, ax4), (ax5, ax6)) = plt.subplots(3, 2, figsize=(20,20))
plt.gray()  # show the filtered result in grayscale
ax1.imshow(im), ax1.axis('off'), ax1.set_title('Original Image', size=20)
ax2.imshow((20*np.log10(0.1 + fftpack.fftshift(im_freq))).real.astype(int))
ax2.set_title('Original Image (Freq Spec)', size=20)
ax3.imshow(noisy), ax3.axis('off'), ax3.set_title('Noisy Image', size=20)
ax4.imshow((20*np.log10( 0.1 + fftpack.fftshift(noisy_freq))).real.astype(int))
ax4.set_title('Noisy Image (Freq Spec)', size=20)

ax5.imshow(noisy_smoothed.real), ax5.axis('off'), ax5.set_title('Output Image (with LPF)', size=20)
ax6.imshow( (20*np.log10( 0.1 + fftpack.fftshift(noisy_smoothed_freq))).real.astype(int))
ax6.set_title('Output Image (Freq Spec)', size=20)
plt.tight_layout()
plt.show()

# ## 3. Blurring an Image with an LPF in the Frequency domain

# ### 3.0 Different Blur Kernels and Convolution in the Frequency domain

def get_gaussian_edge_blur_kernel(sigma, sz=15):
    # First create a 1-D Gaussian kernel
    x = np.linspace(-10, 10, sz)
    kernel_1d = np.exp(-x**2/sigma**2)
    kernel_1d /= np.trapz(kernel_1d) # normalize the sum to 1.0
    # create a 2-D Gaussian kernel from the 1-D kernel
    kernel_2d = kernel_1d[:, np.newaxis] * kernel_1d[np.newaxis, :]
    return kernel_2d

def get_motion_blur_kernel(ln, angle, sz=15):
    kern = np.ones((1, ln), np.float32)
    angle = -np.pi*angle/180
    c, s = np.cos(angle), np.sin(angle)
    A = np.float32([[c, -s, 0], [s, c, 0]])
    sz2 = sz // 2
    A[:,2] = (sz2, sz2) - np.dot(A[:,:2], ((ln-1)*0.5, 0))
    kern = cv2.warpAffine(kern, A, (sz, sz), flags=cv2.INTER_CUBIC)
    return kern

def get_out_of_focus_kernel(r, sz=15):
    kern = np.zeros((sz, sz), np.uint8)
    cv2.circle(kern, (sz, sz), r, 255, -1, cv2.LINE_AA, shift=1)
    kern = np.float32(kern) / 255
    return kern

def dft_convolve(im, kernel):
    F_im = fp.fft2(im)
    #F_kernel = fp.fft2(kernel, s=im.shape)
    F_kernel = fp.fft2(fp.ifftshift(kernel), s=im.shape)
    F_filtered = F_im * F_kernel
    im_filtered = fp.ifft2(F_filtered)
    cmap = 'RdBu'
    plt.figure(figsize=(20,10))
    plt.gray()
    plt.subplot(131), plt.imshow(im), plt.axis('off'), plt.title('input image', size=20)
    plt.subplot(132), plt.imshow(kernel, cmap=cmap), plt.title('kernel', size=20)
    plt.subplot(133), plt.imshow(im_filtered.real), plt.axis('off'), plt.title('output image', size=20)
    plt.tight_layout()
    plt.show()

im = rgb2gray(imread('images/Img_03_03.jpg'))

kernel = get_gaussian_edge_blur_kernel(25, 25)
dft_convolve(im, kernel)

kernel = get_motion_blur_kernel(30, 60, 25)
dft_convolve(im, kernel)

kernel = get_out_of_focus_kernel(15, 20)
dft_convolve(im, kernel)

# ### 3.1 Blurring with scipy.ndimage frequency-domain filters

# #### With fourier_gaussian

im = imread('images/Img_03_31.png')
freq = fp.fft2(im)

fig, axes = plt.subplots(2, 3, figsize=(20,15))
plt.subplots_adjust(0,0,1,0.95,0.05,0.05)
plt.gray() # show the filtered result in grayscale
axes[0, 0].imshow(im), axes[0, 0].set_title('Original Image', size=20)
axes[1, 0].imshow((20*np.log10( 0.1 + fp.fftshift(freq))).real.astype(int)), axes[1, 0].set_title('Original Image Spectrum', size=20)
i = 1
for sigma in [3,5]:
    convolved_freq = ndimage.fourier_gaussian(freq, sigma=sigma)
    convolved = fp.ifft2(convolved_freq).real # the imaginary part is an artifact
    axes[0, i].imshow(convolved)
    axes[0, i].set_title(r'Output with FFT Gaussian Blur, $\sigma$={}'.format(sigma), size=20)
    axes[1, i].imshow((20*np.log10( 0.1 + fp.fftshift(convolved_freq))).real.astype(int))
    axes[1, i].set_title(r'Spectrum with FFT Gaussian Blur, $\sigma$={}'.format(sigma), size=20)
    i += 1
for a in axes.ravel():
    a.axis('off')
plt.show()

# #### With fourier_uniform

im = imread('images/Img_03_31.png')
freq = fp.fft2(im)
freq_uniform = ndimage.fourier_uniform(freq, size=10)

fig, (axes1, axes2) = plt.subplots(1, 2, figsize=(20,10))
plt.gray() # show the result in grayscale
im1 = fp.ifft2(freq_uniform)
axes1.imshow(im), axes1.axis('off')
axes1.set_title('Original Image', size=20)
axes2.imshow(im1.real) # the imaginary part is an artifact
axes2.axis('off')
axes2.set_title('Blurred Image with Fourier Uniform', size=20)
plt.tight_layout()
plt.show()

plt.figure(figsize=(10,10))
plt.imshow( (20*np.log10( 0.1 + fp.fftshift(freq_uniform))).real.astype(int))
plt.title('Frequency Spectrum with fourier uniform', size=20)
plt.show()

# #### With fourier_ellipsoid

freq_ellipsoid = ndimage.fourier_ellipsoid(freq, size=10)
im1 = fp.ifft2(freq_ellipsoid)

fig, (axes1, axes2) = plt.subplots(1, 2, figsize=(20,10))
axes1.imshow(im), axes1.axis('off')
axes1.set_title('Original Image', size=20)
axes2.imshow(im1.real) # the imaginary part is an artifact
axes2.axis('off')
axes2.set_title('Blurred Image with Fourier Ellipsoid', size=20)
plt.tight_layout()
plt.show()

plt.figure(figsize=(10,10))
plt.imshow( (20*np.log10( 0.1 + fp.fftshift(freq_ellipsoid))).real.astype(int))
plt.title('Frequency Spectrum with Fourier ellipsoid', size=20)
plt.show()

# ### 3.2 Gaussian Blur LowPass Filter with scipy.fftpack

#im = rgb2gray(imread('images/Img_03_04.jpg'))
im = rgb2gray(imread('images/Img_03_11.jpg'))
freq = fp.fft2(im)

kernel = np.outer(signal.gaussian(im.shape[0], 1), signal.gaussian(im.shape[1], 1))
#assert(freq.shape == kernel.shape)

freq_kernel = fp.fft2(fp.ifftshift(kernel))

convolved = freq*freq_kernel # by the Convolution theorem

im_blur = fp.ifft2(convolved).real
im_blur = 255 * im_blur / np.max(im_blur)

plt.figure(figsize=(20,20))
plt.subplot(221), plt.imshow(kernel, cmap='coolwarm'), plt.colorbar()
plt.title('Gaussian Blur Kernel', size=20)

plt.subplot(222)
plt.imshow( (20*np.log10( 0.01 + fp.fftshift(freq_kernel))).real.astype(int), cmap='inferno')
plt.colorbar()
plt.title('Gaussian Blur Kernel (Freq. Spec.)', size=20)
plt.subplot(223), plt.imshow(im, cmap='gray'), plt.axis('off'), plt.title('Input Image', size=20)
plt.subplot(224), plt.imshow(im_blur, cmap='gray'), plt.axis('off'), plt.title('Output Blurred Image', size=20)
plt.tight_layout()
plt.show()

def plot_3d(X, Y, Z, cmap=plt.cm.seismic):
    fig = plt.figure(figsize=(20,20))
    ax = fig.gca(projection='3d')
    # Plot the surface.
    surf = ax.plot_surface(X, Y, Z, cmap=cmap, linewidth=5, antialiased=False)
    #ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10)
    #ax.set_zscale("log", nonposx='clip')
    #ax.zaxis.set_scale('log')
    ax.zaxis.set_major_locator(LinearLocator(10))
    ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))
    ax.set_xlabel('F1', size=30)
    ax.set_ylabel('F2', size=30)
    ax.set_zlabel('Freq Response', size=30)
    #ax.set_zlim((-40,10))
    # Add a color bar which maps values to colors.
    fig.colorbar(surf) #, shrink=0.15, aspect=10)
    #plt.title('Frequency Response of the Gaussian Kernel')
    plt.show()

Y = np.arange(freq.shape[0]) #-freq.shape[0]//2,freq.shape[0]-freq.shape[0]//2)
X = np.arange(freq.shape[1]) #-freq.shape[1]//2,freq.shape[1]-freq.shape[1]//2)
X, Y = np.meshgrid(X, Y)
Z = (20*np.log10( 0.01 + fp.fftshift(freq_kernel))).real
plot_3d(X,Y,Z)

Z = (20*np.log10( 0.01 + fp.fftshift(freq))).real
plot_3d(X,Y,Z)

Z = (20*np.log10( 0.01 + fp.fftshift(convolved))).real
plot_3d(X,Y,Z)

# ### 3.3 Convolution in frequency domain with a colored image using fftconvolve from scipy signal

from skimage import img_as_float
from scipy import signal
im = img_as_float(plt.imread('images/Img_03_07.jpg'))

kernel = get_gaussian_edge_blur_kernel(sigma=10, sz=15)
im1 = signal.fftconvolve(im, kernel[:, :, np.newaxis], mode='same')
im1 = im1 / np.max(im1)

kernel = np.array([[0,-1,0],[-1,4,-1],[0,-1,0]])
im2 = signal.fftconvolve(im, kernel[:, :, np.newaxis], mode='same')
im2 = im2 / np.max(im2)
im2 = np.clip(im2, 0, 1)

plt.figure(figsize=(20,10))
plt.subplot(131), plt.imshow(im), plt.axis('off'), plt.title('original image', size=20)
plt.subplot(132), plt.imshow(im1), plt.axis('off'), plt.title('output with Gaussian LPF', size=20)
plt.subplot(133), plt.imshow(im2), plt.axis('off'), plt.title('output with Laplacian HPF', size=20)
plt.tight_layout()
plt.show()

# ## 4. Edge Detection with High-Pass Filters (HPF) in the Frequency domain

def dft2(im):

    freq = cv2.dft(np.float32(im), flags = cv2.DFT_COMPLEX_OUTPUT)
    freq_shift = np.fft.fftshift(freq)
    mag, phase = freq_shift[:,:,0], freq_shift[:,:,1]

    return mag + 1j*phase

def idft2(freq):

    real, imag = freq.real, freq.imag
    back = cv2.merge([real, imag])
    back_ishift = np.fft.ifftshift(back)
    im = cv2.idft(back_ishift, flags=cv2.DFT_SCALE)
    im = cv2.magnitude(im[:,:,0], im[:,:,1])

    return im

def ideal(sz, D0):
    h, w = sz
    u, v = np.meshgrid(range(-w//2,w//2), range(-h//2,h//2)) #, sparse=True)
    return np.sqrt(u**2 + v**2) > D0

def gaussian(sz, D0):
    h, w = sz
    u, v = np.meshgrid(range(-w//2,w//2), range(-h//2,h//2)) #, sparse=True)
    return 1-np.exp(-(u**2 + v**2)/(2*D0**2))

def butterworth(sz, D0, n=1):
    h, w = sz
    u, v = np.meshgrid(range(-w//2,w//2), range(-h//2,h//2)) #, sparse=True)
    return 1 / (1 + (D0/(0.01+np.sqrt(u**2 + v**2)))**(2*n))

def plot_HPF(im, f, D0s):
    freq = dft2(im)
    fig = plt.figure(figsize=(20,20))
    plt.subplots_adjust(0,0,1,0.95,0.05,0.05)
    i = 1
    for D0 in D0s:
        freq_kernel = f(im.shape, D0)
        convolved = freq*freq_kernel # by the Convolution theorem
        im_convolved = idft2(convolved).real
        im_convolved = (255 * im_convolved / np.max(im_convolved)).astype(np.uint8)
        plt.subplot(2,2,i)
        last_axes = plt.gca()
        img = plt.imshow((20*np.log10(0.01 + freq_kernel)).astype(int), cmap='coolwarm')
        divider = make_axes_locatable(img.axes)
        cax = divider.append_axes("right", size="5%", pad=0.05)
        fig.colorbar(img, cax=cax)
        plt.sca(last_axes), plt.title('{} HPF Kernel (freq)'.format(f.__name__), size=20)
        plt.subplot(2,2,i+2), plt.imshow(im_convolved), plt.axis('off')
        plt.title(r'output with {} HPF ($D_0$={})'.format(f.__name__, D0), size=20)
        i += 1
    plt.show()

def plot_HPF_3d(im, f, D0s):
    freq = dft2(im)
    fig = plt.figure(figsize=(20,10))
    plt.subplots_adjust(0,0,1,0.95,0.05,0.05)
    i = 1
    for D0 in D0s:
        freq_kernel = f(im.shape, D0)
        convolved = freq*freq_kernel # by the Convolution theorem
        Y = np.arange(freq_kernel.shape[0])
        X = np.arange(freq_kernel.shape[1])
        X, Y = np.meshgrid(X, Y)
        Z = (20*np.log10( 0.01 + convolved)).real
        ax = fig.add_subplot(1, 2, i, projection='3d')
        surf = ax.plot_surface(X, Y, Z, cmap=plt.cm.coolwarm, linewidth=0, antialiased=False)
        ax.zaxis.set_major_locator(LinearLocator(10)), ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))
        ax.set_xlabel('F1', size=30), ax.set_ylabel('F2', size=30)
        plt.title(r'output with {} HPF (freq)'.format(f.__name__, D0), size=20)
        fig.colorbar(surf, shrink=0.5, aspect=10)
        i += 1
    plt.show()

def plot_filter_3d(sz, f, D0s, cmap=plt.cm.coolwarm):
    fig = plt.figure(figsize=(20,10))
    plt.subplots_adjust(0,0,1,0.95,0.05,0.05)
    i = 1
    for D0 in D0s:
        freq_kernel = f(sz, D0)
        Y = np.arange(freq_kernel.shape[0])
        X = np.arange(freq_kernel.shape[1])
        X, Y = np.meshgrid(X, Y)
        Z = (20*np.log10( 0.01 + freq_kernel)).real
        ax = fig.add_subplot(1, 3, i, projection='3d')
        surf = ax.plot_surface(X, Y, Z, cmap=cmap, linewidth=0, antialiased=False)
        ax.zaxis.set_major_locator(LinearLocator(10))
        ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))
        ax.set_xlabel('F1', size=30)
        ax.set_ylabel('F2', size=30)
        ax.set_title('{} HPF Kernel (freq)'.format(f.__name__), size=20)
        fig.colorbar(surf, shrink=0.5, aspect=10)
        i += 1

im = plt.imread('images/Img_03_12.png')
im = rgb2gray(im)
plt.figure(figsize=(7,12))
plt.imshow(im), plt.axis('off'), plt.title('original image')
plt.show()

D0 = [10, 30]

plot_HPF(im, ideal, D0)

plot_HPF_3d(im, ideal, D0)

plot_filter_3d(im.shape, ideal, D0)

plot_HPF(im, gaussian, D0)

plot_HPF_3d(im, gaussian, D0)

plot_filter_3d(im.shape, gaussian, D0)

plot_HPF(im, butterworth, D0)

plot_HPF_3d(im, butterworth, D0)

plot_filter_3d(im.shape, butterworth, D0)

# ## 5. Implement Homomorphic Filters

from skimage.filters import sobel, threshold_otsu

def homomorphic_filter(im, D0, g_l=0, g_h=1, n=1):
    im_log = np.log(im.astype(np.float)+1)
    im_fft = dft2(im_log)
    H = (g_h - g_l) * butterworth(im.shape, D0, n) + g_l
    #H = np.fft.ifftshift(H)
    im_fft_filt = H*im_fft
    #im_fft_filt = np.fft.ifftshift(im_fft_filt)
    im_filt = idft2(im_fft_filt)
    im = np.exp(im_filt.real)-1
    im = np.uint8(255*im/im.max())
    return im

image = rgb2gray(imread('images/Img_03_13.jpg'))
image_filtered = homomorphic_filter(image, D0=30, n=2, g_l=0.3, g_h=1)

image_edges = sobel(image)
image_edges = image_edges <= threshold_otsu(image_edges)

image_filtered_edges = sobel(image_filtered)
image_filtered_edges = image_filtered_edges <= threshold_otsu(image_filtered_edges)

plt.figure(figsize=(21,17))
plt.gray()
plt.subplots_adjust(0,0,1,0.95,0.01,0.05)
plt.subplot(221), plt.imshow(image), plt.axis('off'), plt.title('original image', size=20)
plt.subplot(222), plt.imshow(image_filtered), plt.axis('off'), plt.title('filtered image', size=20)
plt.subplot(223), plt.imshow(image_edges), plt.axis('off'), plt.title('original image edges', size=20)
plt.subplot(224), plt.imshow(image_filtered_edges), plt.axis('off'), plt.title('filtered image edges', size=20)
plt.show()


import numpy as np
import matplotlib.pyplot as plt
import traitlets
from scipy.integrate import ode, odeint
import ipywidgets as widgets
from IPython.display import display, HTML
import matplotlib.animation as animation

##########

# coursebook examples

##########

def ode0_ex_1_1_func(Tobj, t, Troom, alpha):
	dTdt = -1.*alpha*(Tobj-Troom)
	return dTdt

def ode0_ex_1_1(Tini,Troom,alpha,t):
	T = odeint(ode0_ex_1_1_func, Tini, t, args=(Troom, alpha))
	plt.xlabel("t")
	plt.ylabel("T(C)")
	plt.title("Newton's Law of Cooling")
	plt.plot(t,T)

def ode0_ex_1_2_func(theta, t, g, l):
	return [theta[1], -1.*(g/l)*np.sin(theta[0])]

def ode0_ex_1_2(theta1_ini,theta2_ini, t, g, l):
	theta0 = [theta1_ini, theta2_ini]
	theta = odeint(ode0_ex_1_2_func, theta0, t, args=(g,l))
	plt.xlabel("t")
	plt.ylabel("Angular displacement, (radians)")
	plt.title("Pendulum Anglular Displacement")
	plt.plot(t,theta[:,0])

##########

# convolution

##########

#  convolution functions

def shortSW(t):
    return [1*(val > 0 and val < 2) for val in t]

def longSW(t):
    return [1*(val > 0 and val < 4) for val in t]

def poly(t):
    return -0.25*(t)*(t-4)

def convolveStep(func1, func2, tao):
	"""
		Compute one step of convolution

		----------

		Parameters

		----------

		func1: callable
			function to convolve

		func2: callable
			function to convolve

		tao: float
			dummy variable. Shift in t
	"""
	step = np.multiply(func1, func2)

	return np.trapz(step, x=tao)

def convoluteTo(f1, f2, t, tao):
	"""
		Convolve based on t values to tmax

		----------

		Parameters

		----------

		func1: callable
			function to convolve

		func2: callable
			function to convolve

		t: array_like
			time samples

		tao: float
			dummy variable. Shift in t

		----------

		Returns

		----------

		out: array_like
			convolved function
	"""
	out = []
	for i in t:
		out.append(np.trapz(np.multiply(f1(i-tao), f2(tao)), x=tao))

	return out

def convMain(tmax, func):
	"""
		Main function for producing convolution and plotting result

		----------

		Parameters

		----------

		tmax: float
			max t value

		func: string
			function key for selecting function
	"""
	_, (ax1, ax2) = plt.subplots(2, figsize=(16,8))

	f1 = shortSW
	tao = np.linspace(-10, 10, 1000)
	t = np.arange(-5, tmax, 0.5)

	if func=='f1':
		func2 = shortSW(tmax-tao-0.5)
		f2 = shortSW

	elif func=='f2':
		func2 = longSW(tmax-tao-0.5)
		f2 = longSW

	elif func=='f3':
		func2 = poly(tmax-tao-1.5)
		f2 = poly

	func1 = f1(tao)
	conv = convoluteTo(f1, f2, t, tao)

	ax1.plot(tao, func1)
	ax1.plot(tao, func2)
	ax1.set_xlim(-10, 10)
	ax1.set_ylim(0, 1.2)

	ax2.plot(t, conv)
	ax2.set_xlim(-10, 10)
	ax2.set_ylim(0, 4)

	ax1.set_xlabel('Tau')
	ax1.yaxis.set_ticks([])
	ax1.xaxis.set_ticks([])
	ax1.set_title('Functions showing t shift')
	ax2.set_xlabel('t')
	ax2.set_title('Convolution')
	ax2.yaxis.set_ticklabels([])
	ax2.xaxis.set_ticklabels([])

	plt.show()

def Convolution():
	"""
		main convolution funtion called from notebook
	"""
	t_sldr = widgets.FloatSlider(value=-5, min=-5, max=7, step=0.5, description='$t$', continuous_update=False)
	#  f3 doesn't really work properly
	# f_drp = widgets.Dropdown(options=['f1', 'f2', 'f3'])
	f_drp = widgets.Dropdown(options=['f1', 'f2'])

	return widgets.VBox([widgets.HBox([t_sldr, f_drp]), widgets.interactive_output(convMain, {'tmax':t_sldr, 'func':f_drp})])

##########

# numerical methods

##########

#  derivatives and solutions for solver
def dydx1(x, y, *args):
	return -y+1

def y1(x):
	return 1 - np.exp(-x)

def dydx2(x, y, *args):
	return x*np.exp(np.sin(x))

def dydx3(x, y, *args):
	return np.cos(x)

def y3(x):
	return np.sin(x)

def euler_step(f, xk, yk, h, pars=[]):
	''' Compute a single Euler step. From David Dempsey/ENGSCI 233

		Parameters
		----------
		f : callable
			Derivative function.
		xk : float
			Independent variable at beginning of step.
		yk : float
			Solution at beginning of step.
		h : float
			Step size.
		pars : iterable
			Optional parameters to pass to derivative function.

		Returns
		-------
		yk1 : float
			Solution at end of the Euler step.
	'''
	# evaluate derivative at point
	# step by h in direction of derivative
	# return value of new point
	return yk + h * f(xk, yk, *pars)

def euler_solve(f, x0, y0, x1, h, pars = []):
	''' Compute solution of initial value ODE problem using Euler method.
		From David Dempsey/ENGSCI 233

		----------

		Parameters

		----------

		f : callable
			Derivative function.

		x0 : float
			Initial value of independent variable.

		y0 : float
			Initial value of solution.

		x1 : float
			Final value of independent variable.

		h : float
			Step size.

		pars : iterable
			Optional parameters to pass into derivative function.

		----------

		Returns

		----------

		xs : array-like
			Independent variable at solution.
		ys : array-like
			Solution.

	'''

	# initialise
	nx = int(np.ceil((x1-x0)/h))		# compute number of Euler steps to take
	xs = x0+np.arange(nx+1)*h			# x array
	ys = 0.*xs							# array to store solution
	ys[0] = y0							# set initial value

	for i in range(len(xs)-1):
		ys[i+1] = euler_step(f, xs[i], ys[i], h, pars)

	return xs, ys

def improved_euler_step(f, xk, yk, h):
	"""
		Completes one step of the improved euler method

		----------

		Parameters

		----------

		f: callable
			differential equation to solve

		xk: float
			x ordinate for step start

		tk: float
			y ordinate for step start

		h: float
			step size

		----------

		Returns

		----------
		float: Approximated yk+1 value

	"""

	return yk + h * 0.5*(f(xk, yk) + f(xk + h, yk + h * f(xk, yk)))

def improved_euler_solve(f, x0, y0, x1, h):
	"""
		solves a differential equation using improved euler

		----------

		Parameters

		----------

		f: callable
			differential equation to solve

		x0: float
			inital x value

		y0: float
			inital y value

		x1: float
			upper x value

		h: float
			step size

		----------

		Returns

		----------
		xs: array_like
			x time steps used

		ys: approximated y values

	"""
	# initialise
	nx = int(np.ceil((x1-x0)/h))		# compute number of Euler steps to take
	xs = x0+np.arange(nx+1)*h			# x array
	ys = 0.*xs							# array to store solution
	ys[0] = y0							# set initial value

	for i in range(len(xs)-1):
		# solve for next value in function
		ys[i+1] = improved_euler_step(f, xs[i], ys[i], h)

	return xs, ys

def runge_step(f, xk, yk, h, *args):
	"""
		completes a single step of the runge cutter method (K4)

		----------

		Parameters

		----------

		f: callable
			differential equation to solve

		xk: float
			x ordinate for step start

		tk: float
			y ordinate for step start

		h: float
			step size

		----------

		Returns

		----------

		float: Approximated yk+1 value
	"""
	k1 = h*f(xk, yk, *args)
	k2 = h*f(xk + h/2, yk + k1/2, *args)
	k3 = h*f(xk + h/2, yk + k2/2, *args)
	k4 = h*f(xk + h, yk + k3, *args)

	return yk + (1/6)*(k1 + 2*k2 + 2*k3 + k4)

def runge_solve(f, x0, y0, x1, h, *args):
	'''
		solves a differential equation using runge cutter

		----------

		Parameters

		----------

		f: callable
			differential equation to solve

		x0: float
			x inital condition

		y0: float
			y initial condition

		x1: float
			x stopping point

		h: float
			step size

		----------

		Returns

		----------

		xs: array_like
			x time steps used

		ys: approximated y values
	'''
	nx = int(np.ceil((x1-x0)/h))
	xs = x0+np.arange(nx+1)*h
	ys = 0.*xs
	ys[0] = y0

	for i in range(len(xs)-1):
		ys[i+1] = runge_step(f, xs[i], ys[i], h, args)

	return xs, ys

def plotTangentAtPoint(ax, f, x0, y0, xMin, xMax, h, label=None, color='b'):
	''' plots line on axis given function derivative and x/y points

		----------

		Parameters

		----------

		ax: matplotlib object
			axis to plot on
		f: callable
			derivative function
		x: float
			x ordinate of point
		y: float
			y ordinate of point

	'''
	# plots in range 0 < x < 10

	x = np.arange(xMin,xMax,1)
	m = f(x0,y0)
	y = m*(x-x0) + y0
	if label is not None:
		ax.plot(x,y, ls='--', linewidth=1, label=label, color=color)
	else:
		ax.plot(x,y, ls='--', linewidth=1, color=color)

def RunMethod(h, steps, eqnKey, method, showSln, showGrad):
	''' Runs demo of particular numerical method show

		----------

		Parameters

		----------

        h: float
			step size

        steps: int
			no. steps to take

        eqnKey: str
			key for dict of equtions to use

        method: str
			string key for which method to use

        showSln: bool
			bool for whether to show analytic sln

        showGrad: bool
			bool for whether to show ext grad evals
	'''

	xMin = 0
	xMax = 10
	yMin = 0

	_ = plt.figure(figsize=(16,8))
	ax = plt.axes()
	# get equation
	equation = {"dydx = -y +1":[dydx1, y1, 0, xMax, 0, 1.2], "dy/dx = cos(x)":[dydx3, y3, 0, xMax, -1.5, 1.5]}
	f, soln, xMin, xMax, yMin, yMax = equation[eqnKey]

	xStop = h * steps
	if xStop > xMax: xStop = xMax

	if method=='Improved Euler': # call to improved euler
		x, y = improved_euler_solve(f, 0, 0, xStop, h)
		# for displaying next gradient evaluations
		if showGrad:
			plotTangentAtPoint(ax,f,x[-1]+h,euler_step(f, x[-1], y[-1], h), x[-1]+h, xMax,h, label='Corrector Step', color='r')
			plotTangentAtPoint(ax,f,x[-1],y[-1], x[-1], xMax,h, label='Predictor Step', color='b')
	elif method=='Euler':# call to euler
		x, y = euler_solve(f, 0, 0, xStop, h)
		# for displaying next gradient evaluations
		if showGrad:
			plotTangentAtPoint(ax,f,x[-1],y[-1], xMin, xMax,h, label='Gradient Evaluation')
	elif method=='Runge Cutter': # call to runge cutter
		x, y = runge_solve(f, 0, 0, xStop, h)

	# plot numerical
	ax.plot(x, y, marker='o',color='k', label='Numerical Solution')

	# generate values for error
	xPlot = np.arange(0, xMax, 0.1)
	ySoln = soln(xPlot)
	error = sum((soln(x)[:len(y)]-y)**2)
	# add text showing error
	ax.text(0.01,0.95, 'Total sum of squares error = {:.2e}'.format(error), transform=ax.transAxes, size=20)

	# plot analytic
	if showSln: ax.plot(xPlot,ySoln, label='Analytic Solution')

	plt.xlim(xMin, xMax)
	plt.ylim(yMin, yMax)
	ax.legend(loc=1)
	plt.show()

def NumericalMethods():
	"""
		Main function for called by notebook to produce plots and UI
	"""
	steps_sldr = widgets.IntSlider(value=3, description='Steps', min=0, max=50, continuos_update=False)
	h_sldr = widgets.FloatSlider(value=0.5, description='Step Size', min=0.1, max=1, step=0.1,)
	solver_drp = widgets.Dropdown(options=['Euler', 'Improved Euler'], description='Solver Type')
	eqn_drp = widgets.Dropdown(options=["dydx = -y +1", "dy/dx = cos(x)"], description='Derivative')
	showSln_chk = widgets.Checkbox(value=False, description='Show Solution')
	grad_chk = widgets.Checkbox(value=False, description='Show Next Gradient Evaluations')

	return widgets.VBox([
		widgets.VBox([
			widgets.HBox([
				h_sldr,
				eqn_drp,
				grad_chk]),
			widgets.HBox([
				steps_sldr,
				solver_drp,
				showSln_chk])]),
		widgets.interactive_output(RunMethod, {
			'h':h_sldr,
			'steps':steps_sldr,
			'method':solver_drp,
			'eqnKey':eqn_drp,
			'showSln':showSln_chk,
			'showGrad':grad_chk})
			])

##########

# laplace transforms

# t* = function in t domain
# s* function in s domain

# probably a much better way of doing this. Maybe function class with laplace and t attributes?

##########

def tPoly(t, n, tShift, sShift):
	full = (t-tShift)**n*np.exp(sShift*t)
	return [0 if ((t[i] < 0) or (t[i] < tShift)) else full[i] for i in range(len(t))]

def sPoly(s, n, tShift, sShift):
	full = np.exp(-tShift*s)*np.math.factorial(n)/(s-sShift)**(n+1)
	return [0 if ((s[i] < 0) or (s[i] < sShift)) else full[i] for i in range(len(s))]

def tSin(t, w, tShift, sShift):
	full = np.sin(w*(t-tShift))*np.exp(sShift*t)
	return [0 if ((t[i] < 0) or (t[i] < tShift)) else full[i] for i in range(len(t))]

def sSin(s, w, tShift, sShift):
	full = np.exp(-tShift*s)*(w/((s-sShift)**2 + w**2))
	return [0 if ((s[i] < 0) or (s[i] < sShift)) else full[i] for i in range(len(s))]

def tCos(t, w, tShift, sShift):
	full = np.cos(w*(t-tShift))*np.exp(sShift*t)
	return [0 if ((t[i] < 0) or (t[i] < tShift)) else full[i] for i in range(len(t))]

def sCos(s, w, tShift, sShift):
	full = np.exp(-tShift*s)*(s-sShift)/((s-sShift)**2 + w**2)
	return [0 if ((s[i] < 0) or (s[i] < sShift)) else full[i] for i in range(len(s))]

def tExp(t, a, tShift, sShift):
	full = np.exp(a*(t-tShift))*np.exp(sShift*t)
	return [0 if ((t[i] < 0) or (t[i] < tShift)) else full[i] for i in range(len(t))]

def sExp(s, a, tShift, sShift):
	full = np.exp(-tShift*s)/((s-sShift)-a)
	return [0 if ((s[i] < 0) or (s[i] < sShift)) else full[i] for i in range(len(s))]

def Laplace():
	"""
		Main function called by notebook to produce plots and UI
	"""

	# for ax lim sliders
	tAbsLim = 20
	sAbsLim = 20

	# define widgets
	tlim_sldr = widgets.FloatRangeSlider(value=[-10, 10], step=0.1, min=-tAbsLim, max=tAbsLim, description='t lim', continuous_update=False)
	slim_sldr = widgets.FloatRangeSlider(value=[-10, 10], step=0.1, min=-sAbsLim, max=sAbsLim, description='s lim', continuous_update=False)
	fslim_sldr = widgets.FloatRangeSlider(value=[0, 5], step=0.1, min=-5, max=5, description='F(s) lim', continuous_update=False)
	tShift_sldr = widgets.FloatSlider(value=0, step=0.1, min=-2, max=2, description='Shift t', continuous_updating=False)
	sShift_sldr = widgets.FloatSlider(value=0, step=0.1, min=-2, max=2, description='Shift s', continuous_updating=False)
	func_drop = widgets.Dropdown(options={'t^n':'Poly', 'sin(w*t)':'Sine', 'cos(w*t)':'Cos', 'exp(at)':'exp'}, layout=widgets.Layout(width='220px'), description='$f(t)=$')

	# display
	display(widgets.VBox([
		widgets.HBox([
			widgets.VBox([
				tlim_sldr,
				slim_sldr
			]),
			widgets.VBox([
				tShift_sldr,
				sShift_sldr
			]),
			widgets.VBox([
				fslim_sldr,
				func_drop
			])
		]),
		widgets.interactive_output(showLaplace, {
			'slim':slim_sldr,
			'tlim':tlim_sldr,
			'func':func_drop,
			'tShift':tShift_sldr,
			'fslim':fslim_sldr,
			'sShift':sShift_sldr
			})
		]))

def computeTransform(t, s, slim, tlim, var, tfunc, sfunc, tShift, sShift, fslim):
	"""
		Provides function for interactive output of n or omega to work

		----------

		Parameters

		----------

		t: array_like
			time values

		s: array_like
			s values

		slim: array_like
			pair of s limit values

		tlim: array_like
			pair of t limit values

		var: float
			variable storing either omega or n

		tfunc: callable
			t domain function

		sfunc: callable
			s domain function

		tShift: float
			shift in t

		sShift: float
			 shift in s

		fslim: array_like
			s domain y lim pair
	"""

	ft = tfunc(t, var, tShift, sShift)
	fs = sfunc(s, var, tShift, sShift)

	plotLaplace(t, s, ft, fs, tlim, slim, fslim)

def showLaplace(slim, tlim, func, tShift, sShift, fslim):
	"""
		Chooses function and transform to plot. Produces t samples

		----------

		Parameters

		----------

		slim: array_like
			pair of points for s limits

		tlim: array_like
			pair of points for t limits

		func: str
			string for selected function

		tShift: float
			shift in t

		sShift: float
			 shift in s

		fslim: array_like
			s domain y lim pair
	"""
	t = np.arange(-20, 20, 0.01)
	# s = np.arange(-50, 50 1)
	s = t.copy()
	if func=='Poly':
		var_sldr=widgets.IntSlider(1, min=0, max=10, step=1, description='$n$', continuous_update=False)
		tfunc = tPoly
		sfunc = sPoly

	elif func=='Sine':
		var_sldr=widgets.FloatSlider(1, min=0.1, max=10, step=0.1, description='$\\omega$', continuous_update=False)
		tfunc = tSin
		sfunc = sSin

	elif func=='Cos':
		var_sldr=widgets.FloatSlider(1, min=0.1, max=10, step=0.1, description='$\\omega$', continuous_update=False)
		tfunc = tCos
		sfunc = sCos

	elif func=='exp':
		var_sldr=widgets.FloatSlider(1, min=-2, max=2, step=0.1, description='$a$', continuous_update=False)
		tfunc = tExp
		sfunc = sExp

	display(widgets.VBox([var_sldr, widgets.interactive_output(computeTransform, {
		't':widgets.fixed(t),
		's':widgets.fixed(s),
		'slim':widgets.fixed(slim),
		'tlim':widgets.fixed(tlim),
		'var':var_sldr,
		'tfunc':widgets.fixed(tfunc),
		'sfunc':widgets.fixed(sfunc),
		'tShift':widgets.fixed(tShift),
		'sShift':widgets.fixed(sShift),
		'fslim':widgets.fixed(fslim)
		})]))

def plotLaplace(t, s, ft, fs, tlim, slim, fslim):
	"""
		Main plotting function for laplace demo

		----------

		Parameters

		----------

		t: array_like
			time samples

		s: array_like
			s samples

		ft: array_like
			points in t space

		fs: array_like
			points in s space

		tlim: array_like
			pair of points for t limits

		slim: array_like
			pair of points for s limits

		fslim: array_like
			s domain y lim
	"""

	_, (tAx, sAx) = plt.subplots(1, 2, figsize=(16,8))

	tAx.plot(t, ft)
	tAx.set_xlim(*tlim)
	tAx.set_title('t Domain', fontsize=20)
	tAx.set_xlabel('t', fontsize=16)
	tAx.set_ylabel('f(t)', fontsize=16)

	sAx.plot(s, fs)
	sAx.set_title('s Domain', fontsize=20)
	sAx.set_xlabel('s', fontsize=16)
	sAx.set_xlim(*slim)
	sAx.set_ylim(*fslim)
	sAx.set_ylabel('F(s)', fontsize=16)

	plt.show()

##########

# ode_suspension

##########

def Suspension():
	"""
		Suspension function to be called from notebook.
	"""
	c_sldr = widgets.FloatSlider(3, min=0, max=3, step=0.1, description='c', continuous_update=False)
	m_sldr = widgets.FloatSlider(1, min=0.1, max=3, step=0.1, description='m', continuous_update=False)
	k_sldr = widgets.FloatSlider(1, min=0.1, max=3, step=0.1, description='k', continuous_update=False)

	return (
		widgets.VBox([
			widgets.HBox([
				c_sldr,
				m_sldr,
				k_sldr
			]),
			widgets.interactive_output(suspensionMain, {'c':c_sldr, 'm':m_sldr, 'k':k_sldr})
			])
	)

def suspensionDE(Y, t, m, c, k):
	"""
		Colin Simpson's code...
		But the beautiful docstring is all mine

		Y: array_like
			contains initial conditions

		t: array_like
			time values

		m: float
			mass parameter

		c: float
			damping coeffcient parameter

		k: float
			spring constant parameter
	"""

	return [Y[1], -c*Y[1]-k*Y[0]/m]

def suspensionMain(c, m, k):

	"""
		Built on code from Colin Simpson.
		Main code for computing suspension behaviour and returning values for plotting.

		m: float
			mass parameter

		c: float
			damping coeffcient parameter

		k: float
			spring constant parameter
	"""

	tmin = 0
	tmax = 15
	t = np.arange(tmin, tmax, 0.1)

	# start at y = 1, dydt = 0
	init = [1, 0]

	y = odeint(suspensionDE, init , t, args=(m, c, k))

	if c*c > 4*m*k:
		title = 'y vs t: over-damping'
	elif c*c == 4*m*k:
		title = 'y vs t: critical-damping'
	elif c == 0:
		title = 'y vs t: no-damping'
	else:
		title = 'y vs t: under-damping'

	plotSuspension(y, t, title, tmin, tmax)

def plotSuspension(y, t, title, tmin, tmax):
	"""
		Main function for plotting suspension output

		y: array_like
			vertical displacement

		t: array_like
			time values

		title: string
			graph title

		tmin: float
			minimum time value

		tmax: float maximum time value
	"""

	_ ,ax = plt.subplots(1,1, figsize=(16,8))
	plt.xlabel('t')
	plt.ylabel('y')
	ax.plot(t,y[:,0], color='C0')
	ax.set_title(title, fontsize=20)
	ax.set_ylim(-1.2, 1.2)
	ax.set_xlim(tmin, tmax)

	#ax.plot([t_i],y[1,0],marker='o', markersize=3, color="red")
	plt.show()

##########

# pendulum

##########

def dvdt(a, t, *args):
	"""
		1st equation of system of ODEs

		----------

		Parameters

		----------

		a: float
			angle

		t: float (unused)
			time

		args[0]: g
			acceleration due to gravity

		args[1]: l
			length of pendulum

		----------

		Returns

		----------

		First DE od ode system
	"""
	g, l = args
	return (g/l)*np.sin(a)

def dadt(v, t, *args):
	"""
		1st equation of system of ODEs

		----------

		Parameters

		----------

		a: float
			angle

		t: float (unused)
			time

		----------

		Returns

		----------

		Second DE of ode system
	"""
	return v

def pendUpdate(i, aVec, vVec, line):
	"""
		Update function for animation

		----------

		Parameters

		----------

		i: int
			animation frame number

		aVec: array_like
			a single value. Don't ask why it's an array not just a float

		vVec: array_like
			v

		line: matplotlib object

		----------

		Returns

		----------

		line: matplotlib object
			line updated with new points

	"""
	h = 0.05
	vVec[0] = runge_step(dvdt, aVec[0], vVec[0], h, 9.81, 1)
	aVec[0] = runge_step(dadt, vVec[0], aVec[0], h)
	# print(vVec)
	line.set_data([0, np.sin(aVec)], [0, np.cos(aVec)])
	return line,

def Pendulum(a0=0, v0=0):
	"""
		Main function for calling matplotlib animation

		a0: float
			starting angle (rad)

		v0: float
			starting angular velocity
	"""
	aVec = [a0]
	vVec = [v0]

	fig, ax = plt.subplots(figsize=(12,12))

	line, = ax.plot([0, np.sin(aVec[0])], [0, np.cos(aVec[0])])

	ax.set_xlim(-1.2,1.2)
	ax.set_ylim(-1.2,1.2)
	# frames = 1000 slow to load but good play speed for notebook
	# interval = 5
	anim = animation.FuncAnimation(fig, pendUpdate, frames=150, interval=50, fargs=[aVec, vVec, line], repeat=True, blit=True)

	if __name__=="__main__":
		plt.show()
	else:
		return HTML(anim.to_jshtml())

def pendMain():
	"""
		Main pendulum function called by notebook
	"""
	methods = {'Runge':runge_step, 'Euler':euler_step, 'Improved':improved_euler_step}

	method_drop = widgets.Dropdown(options=methods)

	display(
		method_drop,
		widgets.interactive_output(Pendulum, {})
	)

if __name__=="__main__":
	# pendMain()
	Pendulum()


"""
  Name     : c12_15_simulate_black_Scholes_call.py
  Book     : Python for Finance (2nd ed.)
  Publisher: Packt Publishing Ltd.
  Author   : Yuxing Yan
  Date     : 6/6/2017
  email    : yany@canisius.edu
             paulyxy@hotmail.com
"""
import scipy as sp
from scipy import zeros, sqrt, shape
#
S0 = 40.              # stock price at time zero
X= 40.                # exercise price
T =0.5                # years
r =0.05               # risk-free rate
sigma = 0.2           # annualized volatility
n_steps=100.          # number of steps
#
sp.random.seed(12345) # fix those random numbers
n_simulation = 5000   # number of simulation
dt =T/n_steps
call = sp.zeros([n_simulation], dtype=float)
x = range(0, int(n_steps), 1)
for j in range(0, n_simulation):
    sT=S0
    for i in x[:-1]:
        e=sp.random.normal()
        sT*=sp.exp((r-0.5*sigma*sigma)*dt+sigma*e*sqrt(dt))
        call[j]=max(sT-X,0)
#
call_price=sp.mean(call)*sp.exp(-r*T)
print('call price = ', round(call_price,3))


"""
    Authors: Artur Przybysz (s202384), Elena Mongelli(s181214), Ivan Knezevic (s202386)
"""

# Exercise c

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

# U(x) = e^x;
# U_hat(x) = u_hat1 * N1 + u_hat2 * N2 + u_hat3 * N3

def u_x(x):
    return math.exp(x)

def N1(x, h1):
    if 0 <= x <= h1:
        return (h1 - x) / h1
    else:
        return 0

def N2(x, h1, h2):
    if 0 <= x <= h1:
        return x / h1
    if h1 <= x <= h1 + h2:
        return (h1 + h2 - x) / h2
    else:
        return 0

def N3(x, h1, h2):
    if h1 <= x <= h1 + h2:
        return (x - h1) / h2
    else:
        return 0

def u_hat(x, h1, h2, u_hat1, u_hat2, u_hat3):
    return u_hat1 * N1(x, h1) + u_hat2 * N2(x, h1, h2) + u_hat3 * N3(x, h1, h2)

def u_i(x, h1, h2):
    return u_x(0) * N1(x, h1) + u_x(h1) * N2(x, h1, h2) + u_x(h1 + h2) * N3(x, h1, h2)

if __name__ == '__main__':
    u_hat1 = 1
    u_hat2 = 2.6216
    u_hat3 = math.exp(2)
    h1 = 1
    h2 = 1

    u_hat_values = []
    u_x_values = []
    u_i_values = []
    x = np.arange(0, 2.1, 0.1)
    for i in x:
        u_hat_values.append(u_hat(i, h1, h2, u_hat1, u_hat2, u_hat3))
        u_x_values.append(u_x(i))
        u_i_values.append(u_i(i, h1, h2))

    plt.plot(x, u_hat_values, label="u_hat")
    plt.plot(x, u_x_values, label="u_x")
    plt.plot(x, u_i_values, label="u_i")
    plt.legend()
    plt.show()

    # Exercise d: redefing h1, h2 and u_hat2 since all the other values don't change

    h1 = 4 / 3
    h2 = 2 / 3
    u_hat2 = 3.6996

    u_hat_values = []
    u_x_values = []
    u_i_values = []
    x = np.arange(0, 2.1, 0.1)
    for i in x:
        u_hat_values.append(u_hat(i, h1, h2, u_hat1, u_hat2, u_hat3))
        u_x_values.append(u_x(i))
        u_i_values.append(u_i(i, h1, h2))

    plt.plot(x, u_hat_values, label="u_hat")
    plt.plot(x, u_x_values, label="u_x")
    plt.plot(x, u_i_values, label="u_i")
    plt.legend()
    plt.show()


import numpy as np
import matplotlib.pyplot as plt
from scipy.special import comb

def hypergeom_pmf(N, A, n, x):

    '''
    Probability Mass Function for Hypergeometric Distribution
    :param N: population size
    :param A: total number of desired items in N
    :param n: number of draws made from N
    :param x: number of desired items in our draw of n items
    :returns: PMF computed at x
    '''
    Achoosex = comb(A,x)
    NAchoosenx = comb(N-A, n-x)
    Nchoosen = comb(N,n)

    return (Achoosex)*NAchoosenx/Nchoosen

def hypergeom_cdf(N, A, n, t, min_value=None):

    '''
    Cumulative Density Funtion for Hypergeometric Distribution
    :param N: population size
    :param A: total number of desired items in N
    :param n: number of draws made from N
    :param t: number of desired items in our draw of n items up to t
    :returns: CDF computed up to t
    '''
    if min_value:
        return np.sum([hypergeom_pmf(N, A, n, x) for x in range(min_value, t+1)])

    return np.sum([hypergeom_pmf(N, A, n, x) for x in range(t+1)])

def hypergeom_plot(N, A, n):

    '''
    Visualization of Hypergeometric Distribution for given parameters
    :param N: population size
    :param A: total number of desired items in N
    :param n: number of draws made from N
    :returns: Plot of Hypergeometric Distribution for given parameters
    '''

    x = np.arange(0, n+1)
    y = [hypergeom_pmf(N, A, n, x) for x in range(n+1)]
    plt.plot(x, y, 'bo')
    plt.vlines(x, 0, y, lw=2)
    plt.xlabel('# of desired items in our draw')
    plt.ylabel('Probablities')
    plt.title('Hypergeometric Distribution Plot')
    plt.show()


import numpy as np

def corr_coef(x,y):
    '''It returns the correlation coefficient for two given datasets'''

    x_mean = np.mean(x)
    y_mean = np.mean(y)

    numerator = np.sum((x - x_mean) * (y - y_mean))
    denominator = np.sqrt(np.sum((x - x_mean)*(x - x_mean))) * np.sqrt(np.sum((y - y_mean)*(y - y_mean)))

    r = numerator / denominator

    return r


import numpy as np
import matplotlib.pylab as plt

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

y = np.array([[4],
              [5],
              [6]])

print(x.T@y)

""" Exercise 2.2.1 """
p = np.array([100, 80, 50]).reshape((-1,1))
n = np.array([3, 4, 5]).reshape((-1,1))

w = p.T@n

print("w: {}".format(w))

print("=============================")

from sklearn.datasets import load_digits, load_iris
import matplotlib.gridspec as gridspec

digits = load_digits()
d1 = digits.images[0]
d2 = digits.images[10]
d3 = digits.images[1]
d4 = digits.images[11]
v1 = d1.reshape(64,1)
v2 = d2.reshape(64,1)
v3 = d3.reshape(64,1)
v4 = d4.reshape(64,1)

print("similarity b/w zeros: ", v1.T@v2)
print("similarity b/w ones: ", v3.T@v4)
print("similarity b/w zero and one: ", v1.T@v3)

""" Exercise 2.2.3 """
X = load_digits().data
first_image = digits.images[0].reshape(64,1)
print(first_image.T@X[0]) #1

#2: Similarities of all images
SoAI = np.dot(X, X.T)

X = load_iris().data
print(X[:,0].mean())

from sklearn.datasets import fetch_olivetti_faces

faces = fetch_olivetti_faces()

f, ax = plt.subplots(1,3)

ax[0].imshow(faces.images[6], cmap=plt.cm.bone)
ax[0].grid(False)
ax[0].set_xticks([])
ax[0].set_yticks([])
ax[0].set_title('image 1: $x_1$')

ax[1].imshow(faces.images[10], cmap=plt.cm.bone)
ax[1].grid(False)
ax[1].set_xticks([])
ax[1].set_yticks([])
ax[1].set_title('image 1: $x_2$')

new_face = 0.7 * faces.images[6] + 0.3*faces.images[10]
ax[2].imshow(new_face, cmap=plt.cm.bone)
ax[2].grid(False)
ax[2].set_xticks([])
ax[2].set_yticks([])
ax[2].set_title('image 3: $x_new$')

# plt.show()

""" Exercise 2.4.6 """
from sklearn.datasets import load_boston
boston = load_boston()
X = boston.data
y = boston.target


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Tue Dec 19 16:13:31 2017

@author: saikalyan
"""
import numpy as np

#Features module: contains useful methods to find features of an array

# Returns the number of elements in the given array
def numElements(array):
    # Just returns the length of the array
    return len(array)

# Returns a number from -1.0 (completely descending)
# to 1.0 (completely ascending)
def sortScore(array):
    score = 0.0
    for i in range(0,len(array)-1):
        if array[i] < array[i+1]:
            score += 1.0
        elif array[i] > array[i+1]:
            score -= 1.0
    #Now normalize the sum
    score = score / (len(array)-1)
   # print(score)
    return score

# Returns the percentage of numbers that are unique
# If n = 10, 0.1 means 1 unique val, 1.0 means all unique vals
def uniqueVals(array):
    # Returns an array of the unique numbers, length of
    # this new array is how many unique elements there are
    u = np.unique(array)
   # print(len(u) / len(array))
    return len(u) / len(array)

#Tests
sortScore([5,1,7,3,5,3,8,2])
uniqueVals([3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,1,1,1,1])

a = [1,2,3,4,5]
print("Sort Score: ",sortScore(a))
print("Unique Vals: ",uniqueVals(a))
print("Number of Elements: ",numElements(a))


"""
Locally Weighted Linear Regression (Loess)

see:
https://xavierbourretsicotte.github.io/loess.html
"""

import os
import sys

from math import ceil
import numpy as np
from scipy import linalg

import matplotlib.pyplot as plt
import pandas as pd
##from IPython.display import Image
##from IPython.display import display
#plt.style.use('seaborn-white')
## if jupyter: %matplotlib inline

import statsmodels.api as sm

#Defining the bell shaped kernel function - used for plotting later on
def kernel_function(xi,x0,tau= .005):
    """compute bell kernel"""
    return np.exp( - (xi - x0)**2/(2*tau)   )

def lowess_bell_shape_kern(x, y, tau = .005):
    """lowess_bell_shape_kern(x, y, tau = .005) -> yest
    Locally weighted regression: fits a nonparametric regression curve to a scatterplot.
    The arrays x and y contain an equal number of elements; each pair
    (x[i], y[i]) defines a data point in the scatterplot. The function returns
    the estimated (smooth) values of y.
    The kernel function is the bell shaped function with parameter tau. Larger tau will result in a
    smoother curve.
    """
    n = len(x)
    yest = np.zeros(n)

    #Initializing all weights from the bell shape kernel function
    w = np.array([np.exp(- (x - x[i])**2/(2*tau)) for i in range(n)])

    #Looping through all x-points
    for i in range(n):
        weights = w[:, i]
        b = np.array([np.sum(weights * y), np.sum(weights * y * x)])
        A = np.array([[np.sum(weights), np.sum(weights * x)],
                      [np.sum(weights * x), np.sum(weights * x * x)]])
        theta = linalg.solve(A, b)
        yest[i] = theta[0] + theta[1] * x[i]

    return yest

def lowess_ag(x, y, f=2. / 3., iter=3):
    """lowess(x, y, f=2./3., iter=3) -> yest
    Lowess smoother: Robust locally weighted regression.
    The lowess function fits a nonparametric regression curve to a scatterplot.
    The arrays x and y contain an equal number of elements; each pair
    (x[i], y[i]) defines a data point in the scatterplot. The function returns
    the estimated (smooth) values of y.
    The smoothing span is given by f. A larger value for f will result in a
    smoother curve. The number of robustifying iterations is given by iter. The
    function will run faster with a smaller number of iterations.
    """
    n = len(x)
    r = int(ceil(f * n))
    h = [np.sort(np.abs(x - x[i]))[r] for i in range(n)]
    w = np.clip(np.abs((x[:, None] - x[None, :]) / h), 0.0, 1.0)
    w = (1 - w ** 3) ** 3
    yest = np.zeros(n)
    delta = np.ones(n)

    for iteration in range(iter):
        for i in range(n):
            weights = delta * w[:, i]
            b = np.array([np.sum(weights * y), np.sum(weights * y * x)])
            A = np.array([[np.sum(weights), np.sum(weights * x)],
                          [np.sum(weights * x), np.sum(weights * x * x)]])
            beta = linalg.solve(A, b)
            yest[i] = beta[0] + beta[1] * x[i]

            residuals = y - yest
            s = np.median(np.abs(residuals))
            delta = np.clip(residuals / (6.0 * s), -1, 1)
            delta = (1 - delta ** 2) ** 2

    return yest

def lowess_sm(x, y, f=1. / 3., iter=3):
    """
    ref:
    https://www.statsmodels.org/stable/generated/statsmodels.nonparametric.smoothers_lowess.lowess.html
    """
    lowess_sm_ = sm.nonparametric.lowess
    return lowess_sm_(y, x, f, iter, return_sorted = False)

def smooth(mrun, key, inplace, tau, debug, show, input_file):
    """
    Smooth key column data from mrun dataset
    """

    filteredkey = "filtered%s" % key
    df = mrun.getData()
    Meanval = df[key].mean()

    #Initializing noisy non linear data
    x = df["t"].to_numpy()
    y = df[key].to_numpy()

    if debug:
        plt.figure(figsize=(10,6))
        plt.scatter(x,y, facecolors = 'none', edgecolor = 'darkblue', label = key)

    yest_bell = lowess_bell_shape_kern(x,y,tau)
    if debug:
        plt.plot(x,yest_bell,color = 'red', label = 'Loess: bell shape kernel')
        x0 = (x[0] + x[40])/2.
        plt.fill(x[:40],Meanval*kernel_function(x[:40],x0,tau), color = 'lime', alpha = .5, label = 'Bell shape kernel')
        if show:
            plt.show()
        else:
            f_extension = os.path.splitext(input_file)[-1]
            imagefile = input_file.replace(f_extension, "-smoothed%s.png" % key)
            print("save to %s" % imagefile)
            plt.savefig(imagefile, dpi=300)
        plt.close()

    if inplace:
        # replace key by filtered ones
        df[filteredkey] = pd.Series(yest_bell)
        del df[key]
        df.rename(columns={filteredkey: key}, inplace=True)
    # except:
    #     print("smooth: bell_shape_kern smoother failed")
    return mrun

def filterpikes(mrun, key, inplace, threshold, twindows, debug, show, input_file):
    """
    # filter spikes
    # see: https://ocefpaf.github.io/python4oceanographers/blog/2015/03/16/outlier_detection/
    """

    print("type(mrun):", type(mrun))
    df = mrun.getData() #.extractData(keys)

    kw = dict(marker='o', linestyle='none', color='r', alpha=0.3)
    mean = df[key].mean()
    # print("%s=" % key, type(df[key]), df[key])

    if mean != 0:
        var = df[key].var()
        # print("mean(%s)=%g" % (key,mean), "std=%g" % math.sqrt(var) )
        filtered = df[key].rolling(window=twindows, center=True).median().fillna(method='bfill').fillna(method='ffill')
        filteredkey = "filtered%s" % key
        # print("** ", filteredkey, type(filtered))

        df = df.assign(**{filteredkey: filtered.values})
        # print("*** ", filteredkey, df[filteredkey])

        difference = np.abs((df[key] - filtered)/mean*100)
        outlier_idx = difference > threshold

        if debug:
            ax = plt.gca()
            df[key].plot()
            # filtered.plot()
            df[filteredkey].plot()
            df[key][outlier_idx].plot(**kw)

            ax.legend()
            plt.grid(b=True)
            plt.title(mrun.getInsert().replace(r"_",r"\_") + ": Filtered %s" % key)
            if show:
                plt.show()
            else:
                f_extension = os.path.splitext(input_file)[-1]
                imagefile = input_file.replace(f_extension, "-filtered%s.png" % key)
                print("save to %s" % imagefile)
                plt.savefig(imagefile, dpi=300)
            plt.close()

        if inplace:
            # replace key by filtered ones
            del df[key]
            df.rename(columns={filteredkey: key}, inplace=True)

    return mrun

def lagged_correlation(df, target, key, t):
    """
    Compute lag correlation between target and key df column
    """

    lagged_correlation = df[target].corr(df[key].shift(+t))
    print("type(lagged_correlation):", type(lagged_correlation))
    print("lagged_correlation(t=%g):" % t, lagged_correlation)

if __name__ == "__main__":

    import argparse
    import python_magnetrun

    parser = argparse.ArgumentParser()
    parser.add_argument("input_file")
    parser.add_argument("--keys", help="specify keys to select (eg: Tin1;Tin2)", default="Tin1")
    parser.add_argument("--show", help="display graphs (requires X11 server active)", action='store_true')
    parser.add_argument("--debug", help="activate debug mode", action='store_true')

    # define subparser: filter, smooth, lag_correlation
    subparsers = parser.add_subparsers(title="commands", dest="command", help='sub-command help')

    parser_filter = subparsers.add_parser('filter', help='filter help')
    parser_smooth = subparsers.add_parser('smooth', help='smooth help')
    parser_lag = subparsers.add_parser('lag', help='lag help')

    parser_filter.add_argument("--threshold", help="specify a threshold for filter", type=float, default=0.5)
    parser_filter.add_argument("--twindows", help="specify a window length", type=int, default=10)

    parser_smooth.add_argument("--method", help="select a smoother for data", type=str, choices=['ag','bell_kernel','statsmodel_sm', 'all'], default='bell_kernel')
    parser_smooth.add_argument("--smooth_params", help="pass param for smoother method (eg \"f;iter\")", type=str, default='400')
    # parser.add_argument("--smoothing_f", help="specify smoothing f param", type=float, default=0.25)
    # parser.add_argument("--smoothing_tau", help="specify smoothing tau param", type=float, default=0.005)
    # parser.add_argument("--smoothing_iter", help="specify smoothing iter param", type=int, default=5)

    parser_lag.add_argument("--target", help="specify a target field", type=str, default='tsb')
    parser_lag.add_argument("--trange", help="specify a range for t", type=int, default=100)

    args = parser.parse_args()

    threshold = 0.5
    twindows = 10
    if args.command == 'filter':
        threshold = args.threshold
        twindows = args.twindows

    smoothing_f = 0.7
    smoothing_tau = 400
    smoothing_iter = 3
    if args.command == 'smooth':
        params = args.smooth_params.split(';')
        if args.method == "ag":
            smoothing_f = float(params[0])
            smoothing_iter = int(params[1])
        elif args.method == "bell_kernel":
            smoothing_tau = float(params[0])
        elif args.method == "statsmodel_sm":
            smoothing_tau = float(params[0])
        else:
            smoothing_tau = float(params[0])
            smoothing_f = float(params[1])
            smoothing_iter = int(params[2])

    f_extension=os.path.splitext(args.input_file)[-1]
    if f_extension != ".txt":
        print("so far only txt file support is implemented")
        sys.exit(0)

    filename = os.path.basename(args.input_file)
    result = filename.startswith("M")
    if result:
        try:
            index = filename.index("_")
            args.site = filename[0:index]
            print("site detected: %s" % args.site)
        except:
            print("no site detected - use args.site argument instead")
            pass
    mrun = python_magnetrun.MagnetRun.fromtxt(args.site, args.input_file)
    mrun.getMData().addTime()
    start_timestamp = mrun.getMData().getStartDate()
    dkeys = mrun.getKeys()

    inplace = False
    skeys = args.keys.split(";")
    if args.command == 'filter':
        for key in skeys:
            filterpikes(mrun, key, inplace, threshold, twindows, args.debug, args.show, args.input_file)

    if args.command == 'smooth':
        for key in skeys:
            selected_df = mrun.getMData().extractData(["t", key])
            Meanval = selected_df[key].mean()

            #Initializing noisy non linear data
            x = selected_df["t"].to_numpy() #np.linspace(0,1,100)
            y = selected_df[key].to_numpy() # np.sin(x * 1.5 * np.pi )

            print("display Weighted Linear Regression")
            plt.figure(figsize=(10,6))
            plt.scatter(x,y, facecolors = 'none', edgecolor = 'darkblue', label = key)

            #
            print("compute Locally Weighted Linear Regression %s" % args.method)
            if args.method == "ag":
                try:
                    yest = lowess_ag(x, y, f=smoothing_f, iter=smoothing_iter)
                    plt.plot(x,yest, color = 'orange', label = 'Loess: A. Gramfort')
                except:
                    print("Failed to build lowess_ag")

            if args.method == "bell_kernel":
                try:
                    yest_bell = lowess_bell_shape_kern(x,y,smoothing_tau)
                    if args.debug:
                        x0 = (x[0] + x[40])/2.
                        plt.fill(x[:40],Meanval*kernel_function(x[:40],x0,args.smoothing_tau), color = 'lime', alpha = .5, label = 'Bell shape kernel')
                        plt.plot(x,yest_bell,color = 'red', label = 'Loess: bell shape kernel')
                except:
                    print("Failed to build bell")

            if args.method == "statsmodel_sm":
                try:
                    # f = 0.7 # between 0 and 1, by default: 2./3.
                    yest_sm = lowess_sm(x,y, f=smoothing_f, iter=smoothing_iter)
                    plt.plot(x,yest_sm, color = 'magenta', label = 'Loess: statsmodel') # marker="o",
                except:
                    print("Failed to build sm")

            plt.legend()
            plt.title('Loess regression comparisons')
            if args.show:
                plt.show()
            else:
                imagefile = mrun.getSite()
                start_date = ""
                start_time = ""
                if "Date" in dkeys and "Time" in dkeys:
                    tformat="%Y.%m.%d %H:%M:%S"
                    start_date=mrun.getMData().getData("Date").iloc[0]
                    start_time=mrun.getMData().getData("Time").iloc[0]

                plt.savefig('%s_%s---%s-smoothed-%s.png' % (imagefile,str(start_date),str(start_time),key) , dpi=300 )
            plt.close()

    if args.command == 'lag':
        for key in skeys:
            df = mrun.getData()
            for t in range(args.trange):
                lagged_correlation(df, args.target, key, t)

import numpy,from scipy,import statsmodels


import math
import random

import sympy as sym
import numpy as np
from scipy.optimize import brentq

r, s, x, theta_r, tau = sym.symbols("r, s, x, theta_r, tau", positive=True)

alpha, H, beta, Gamma, F, epsilon = sym.symbols(
    "alpha, H, beta, Gamma, F, epsilon", positive=True
)

def theta(r, s, theta_r=theta_r):
    """
    Return the quantity of horn gained:

    Parameters
    ----------

    s: float between 0 and 1 - the proportion of poachers acting selectively
    r: float between 0 and 1 - the proportion of horns devalued
    theta_r: float between 0 and 1 - the value of dehorned horn
    """
    return s * (1 - r) + (1 - s) * ((theta_r - 1) * r + 1)

def selective_time(r, tau):
    """
    Expected time spent in park by a selective poacher

    Parameters
    ----------

    r: float between 0 and 1 - the proportion of horns devalued
    tau: float between       - the time taken to kill a horn
    """
    return (r + tau * (1 - r)) / (1 - r)

def indiscriminate_time(r, tau, N_r):
    """
    Expected time spent in park by an indiscriminate poacher

    Parameters
    ----------

    r: float between 0 and 1       - the proportion of horns devalued
    tau: float                     - the time taken to kill a horn
    N_r: int                       - number of devalued rhinos that correspond
                                     to a single valued rhino
    """
    return tau * (1 - r ** N_r) / (1 - r)

def gain(s=s, x=x, H=H, r=r, theta_r=theta_r, alpha=alpha):
    """
    Return the total gain of the poachers based on their behaviour.
    """
    return theta(r, s, theta_r) * H * theta(r, x, theta_r) ** (-alpha)

def cost(s=s, tau=tau, x=x, r=r, F=F, beta=beta, theta_r=theta_r):

    N_r = sym.ceiling(1 / theta_r)
    return (
        F
        * (1 - r) ** beta
        * (
            s * selective_time(r=r, tau=tau)
            + (1 - s) * indiscriminate_time(r=r, tau=tau, N_r=N_r)
        )
    )

def utility(
    s=s, x=x, F=F, H=H, r=r, alpha=alpha, beta=beta, tau=tau, theta_r=theta_r,
    Gamma=Gamma
):
    """
    The total utility for a strategy
    sigma=(s, 1-s) in a population chi=(x, 1-x)
    """
    return gain(s=s, x=x, H=H, r=r, theta_r=theta_r, alpha=alpha) - cost(
        s=s, tau=tau, x=x, r=r, F=F, beta=beta, theta_r=theta_r
    ) - (1 - s) * Gamma


import numpy as np

f_src=open('nodes_list.txt','r')

# imported node information
nodes=[]	# info: [{'node':'x', 'x':pos_x, 'z':pos_z}]
len_nodes=0	# amount

while(True):
  linetmp=f_src.readline()
  if(not linetmp):
    break
  line=linetmp.split('\t')
  if(len(line)!=3):
    break
  nodes.append({'node':line[0],'x':int(line[1]),'z':int(line[2])})
len_nodes=len(nodes)

f_src.close()

print("Distance table:")
# n*n distance table: length=dist_table_full[index1][index2]
dist_table_full=np.full((len_nodes, len_nodes), -1, dtype=int)
# n*(n-1)/2 E dict: {'node1':index1, 'node2':index2, 'length':len}
dist_table_dict=[]

print("#\t",end ="")
for i in range(0,len_nodes):
  if(i==len_nodes-1):
    print(nodes[i]['node'],end ="\n")
  else:
    print(nodes[i]['node'],end ="\t")

for i in range(0,len_nodes):
  print(nodes[i]['node'],end ="\t")
  for j in range(0,len_nodes):
    dist_table_full[i][j]=int(pow(pow(nodes[i]['x']-nodes[j]['x'],2)+pow(nodes[i]['z']-nodes[j]['z'],2),1/2))
    if(i < j):
      dist_table_dict.append({'node1':i, 'node2':j, 'length':dist_table_full[i][j]})
    if(j==len_nodes-1):
      print(dist_table_full[i][j],end ="\n")
    else:
      print(dist_table_full[i][j],end ="\t")

print()

## MST: Prim
# G = (V=nodes, E=dist_table_full)
prim_V_a=[]	# target nodes collection
prim_V_b=[]	# source nodes collection
prim_path=[]	# generated path: {'node1':index, 'node2':index}
sum_length=0

for i in range(0,len_nodes):
  prim_V_b.append({'index':i})

prim_V_a.append(prim_V_b.pop())	# init node

while(prim_V_b):
  node_b=prim_V_b.pop()

  connectedto_a_index=-1	# which a should b connected to
  min_length=-1	# total minimum length
  for node_a in prim_V_a:
    target_len=dist_table_full[node_a['index']][node_b['index']]
    if(target_len == -1):
      continue	# skip
    if(min_length==-1):
      min_length=target_len
      connectedto_a_index=node_a['index']
    else:
      if(min_length > target_len):
        min_length = target_len
        connectedto_a_index=node_a['index']
  if(connectedto_a_index == -1):
    print("MST failed for node %d %s : no path valid"%(node_b['index'],nodes[node_b['index']]['node']))
  else:
    sum_length=sum_length+dist_table_full[connectedto_a_index][node_b['index']]
    prim_V_a.append(node_b)
    prim_path.append({'node1':min(connectedto_a_index,node_b['index']), 'node2':max(connectedto_a_index,node_b['index'])})
# MST length table
conn_table_mst=np.full((len(prim_V_a), len(prim_V_a)), 0, dtype=int)
for gen_path in prim_path:
  conn_table_mst[gen_path['node1']][gen_path['node2']]=dist_table_full[gen_path['node1']][gen_path['node2']]
  conn_table_mst[gen_path['node2']][gen_path['node1']]=dist_table_full[gen_path['node2']][gen_path['node1']]
# print table
print("MST table:")
print("#\t",end ="")
for i in range(0,len(prim_V_a)):
  if(i==len(prim_V_a)-1):
    print(nodes[prim_V_a[i]['index']]['node'],end ="\n")
  else:
    print(nodes[prim_V_a[i]['index']]['node'],end ="\t")

for i in range(0,len(prim_V_a)):
  print(nodes[prim_V_a[i]['index']]['node'],end ="\t")
  for j in range(0,len(prim_V_a)):
    if(j==len(prim_V_a)-1):
      print(conn_table_mst[prim_V_a[i]['index']][prim_V_a[j]['index']],end ="\n")
    else:
      print(conn_table_mst[prim_V_a[i]['index']][prim_V_a[j]['index']],end ="\t")
print("Total length: %d"%sum_length)
## MST: Prim

print()

print("MST paths:")

print("From\tTo\tLength")
for path in prim_path:
  print("%s\t%s\t%d"%(nodes[path['node1']]['node'],nodes[path['node2']]['node'],conn_table_mst[path['node1']][path['node2']]))
  print("%s\t%s\t%d"%(nodes[path['node2']]['node'],nodes[path['node1']]['node'],conn_table_mst[path['node2']][path['node1']]))


# The body mass index (BMI) is a number that can be calculated for any individual by using the equation
# BMI = 703w / h**2
# where weight (w) is in pounds and height (h) is in inches.  Use this information to complete the following.

from sympy import symbols, solve, pprint, Derivative
import math

# rounding

dec_places = 0

# input variables

weight = 219
height = 6.2
target_bmi = 24.9

height = int( height ) * 12 + ( height % 1 * 10 )

w, h = symbols( 'w, h' )

fBMI = 703*w / h**2

w_bmi = fBMI.subs( { h: height } )
bmi = w_bmi.subs( { w: weight } )

print( 'raw bmi: {0}'.format( bmi ) )

target_weight = solve( w_bmi - target_bmi, w )[ 0 ]

# a
# Calculate the BMI for a person who weighs 219 pounds and is 6 prime 2 double prime tall.
print( 'A person that weighs {0}lbs and is {1}in tall has a BMI of {2}'.format( weight, height, round( bmi, dec_places ) ) )
# b
# How much weight would the person in part a have to lose to reach a BMI of 24.9?
print( 'They would need to loose {0}lbs to reach a BMI of {1}'.format( round( weight - target_weight, dec_places ), target_bmi ) )

# c
# For a 126-lb. female, what is the rate of change of BMI with respect to height?

weight = 126
dH = Derivative( fBMI.subs( { w: weight } ) ).doit()
pprint( dH )

# d
# Calculate f'( 66 ).
height = 66
rate_of_change = float( dH.subs( { h: height } ) )

print( 'rate of change for female {0}'.format( round( rate_of_change, 2 ) ) )


import math

import numpy as np
import pandas as pd
from scipy.special import gammaln
from sklearn.utils.linear_assignment_ import linear_assignment

def beta(alphas):
    """Multivariate beta function"""
    return math.exp(sum(map(gammaln, alphas)) - gammaln(sum(alphas)))

def cumsum_ex(arr):
    """
    Function computing the cumulative sum exluding the last element for the first element the cumsum is 0
    :param arr: array of shape [p,]
    :return: cum_sum_arr: of shape [p,] where cum_sum_arr[0] = 0 and cum_sum_arr[i] = cumsum(arr[:i])
    """
    cum_sum_arr = np.zeros_like(arr)
    for i in range(len(arr)):
        if i == 0:
            cum_sum_arr[i] = 0
        else:
            cum_sum_arr[i] = np.cumsum(arr[:i])[-1]
    return cum_sum_arr

def cluster_acc(Y_pred, Y):
    """
    Function computing cluster accuracy and confusion matrix at a permutation of the labels
    :param Y_pred: The predicted labels of shape [N, ]
    :param Y: The true labels of shape [N, ]
    :return: Clusterring_accuracy, Confusion matrix
    """
    assert Y_pred.size == Y.size
    D = max(Y_pred.max(), Y.max()) + 1
    w = np.zeros((D, D), dtype=np.int64)
    for i in range(Y_pred.size):
        w[Y_pred[i], Y[i]] += 1
        ind = linear_assignment(w.max() - w)
    return sum([w[i, j] for i, j in ind]) * 1.0 / Y_pred.size, w

def get_ind_function(X):
    """
    Returns a dictionary of indicator function for values taken by the random variables over the vocabulary of each
    dimension
    :param X: data frame of shape [N, d]
    :return: dict_C: for each X_i the indicator of values taken of shape [N, |X_i|]
    """
    dict_C = {}
    N = X.shape[0]
    for column in X.columns:
        vocabulary = np.unique(X[column])
        C = np.zeros(shape=(N, len(vocabulary)))
        for i in range(len(vocabulary)):
            v = vocabulary[i]
            C[:, i] = (X[column].values == v).astype(np.int32)
        dict_C[column] = C

    return dict_C

def normalize(params, axis=0):
    """
    Function normalizing the parameters vector params with respect to the Axis: axis
    :param params: array of parameters of shape [axis0, axis1, ..., axisp] p can be variable
    :return: params: array of same shape normalized
    """

    return params / np.sum(params, axis=axis, keepdims=True)

def most_accuring_terms(df):
    """
    Function returning the most accruing assignments in a dataframe
    :param df: dataframe of shape [N, d]
    :return: list of most accruing assignments
    """
    df_made = pd.DataFrame({col: str(col) + '=' for col in df}, index=df.index) + df.astype(str)
    accuring_asignments = pd.Series(np.reshape(df_made.values.tolist(), [-1])).value_counts()[:]
    return accuring_asignments


import numpy as np
# from scipy import stats, optimize

def PolyBaseFunction(x, dims):
    return np.array([x**d for d in range(dims+1)]).T

class LinearRegression:
    def __init__(self, y, Phi):
        self.y = np.array(y).reshape((-1, 1))
        self.Phi = np.array(Phi)

        self.N, self.D = self.Phi.shape

    def fit_OLS(self):
        self.beta_OLS \
            = np.linalg.pinv(self.Phi.T @ self.Phi) @ self.Phi.T @ self.y
        diff = self.y - self.predict_OLS(self.Phi)
        self._loss_OLS = float(diff.T @ diff)

    def predict_OLS(self, Phi):
        return Phi @ self.beta_OLS

    def fit_ML(self):
        self.fit_OLS()
        self.beta_ML = self.beta_OLS
        self.sigma2_ML = self._loss_OLS / self.N
        self._loglik_ML = -0.5*(self._loss_OLS / self.sigma2_ML
                                + self.N * np.log(2*np.pi)
                                + self.N * np.log(self.sigma2_ML))

    def predict_ML(self, Phi):
        return self.predict_OLS(Phi)

    def fit_Ridge(self, alpha):
        self.alpha_Ridge = float(alpha)
        inversed = np.linalg.pinv(self.Phi.T @ self.Phi
                                  + self.alpha_Ridge*np.eye(self.D))
        self.beta_Ridge \
            = inversed @ self.Phi.T @ self.y
        diff = self.y - self.predict_Ridge(self.Phi)
        self._loss_Ridge = float(diff.T @ diff) \
            + self.alpha_Ridge * self._penalty_Ridge(self.beta_Ridge)

    def predict_Ridge(self, Phi):
        return Phi @ self.beta_Ridge

    def _penalty_Ridge(self, beta):
        return float(beta.T @ beta)

    def fit_Bayes(self, sigma2, prior_cov):
        self.sigma2_Bayes = float(sigma2)
        self.prior_cov_Bayes = np.array(prior_cov).reshape((self.D, self.D))
        self._prior_prec_Bayes = np.linalg.pinv(self.prior_cov_Bayes)

        self._beta_prec_Bayes \
            = self._prior_prec_Bayes \
            + self.Phi.T @ self.Phi / self.sigma2_Bayes
        self.beta_cov_Bayes = np.linalg.pinv(self._beta_prec_Bayes)
        self.beta_mean_Bayes = \
            self.beta_cov_Bayes @ self.Phi.T @ self.y / self.sigma2_Bayes

    def predict_Bayes(self, Phi):
        N = len(Phi)

        mu = (Phi @ self.beta_mean_Bayes).flatten()
        sigma2 = np.empty(N)
        for n in range(N):
            sigma2[n] = 1 / self.sigma2_Bayes \
                + Phi[n].T @ self.beta_cov_Bayes @ Phi[n]

        return mu, sigma2


import numpy as np
from scipy.linalg import svd

def pca(X, n_components=5):
    #  Use svd to perform PCA on X
    #  Inputs:
    #     X: input matrix
    #     n_components: number of principal components to keep
    #  Output:
    #     principal_components: the top n_components principal_components
    #     X_pca = X.dot(principal_components)

    U, s, Vh = svd(X)

    # Write your own code
    principal_components = "Write your own code"
    return principal_components

def normalize(X, norm="Min-Max", axis = 1):
    #  Inputs:
    #     X: input matrix
    #     norm = {"L1", "L2", "Min-Max", "Standard_Score"}
    #     axis = 0: normalize rows
    #     axis = 1: normalize columns
    #  Output:
    #     X_norm: normalized matrix (numpy.array)
    #  Write your own code below:
    X_array = np.asarray(X)
    return X_norm

def stratified_sampling(y, ratio, replace = True):
    #  Inputs:
    #     y: class labels
    #     0 < ratio < 1: number of samples = len(y) * ratio
    #     replace = True: sample with replacement
    #     replace = False: sample without replacement
    #  Output:
    #     sample: indices of stratified sampled points
    #             (ratio is the same across each class,
    #             samples for each class = int(np.ceil(ratio * # data in each class)) )

    if ratio<=0 or ratio>=1:
        raise Exception("ratio must be 0 < ratio < 1.")
    y_array = np.asarray(y)
    # Write your own code below

    return sample.astype(int)


import numpy as np

class _Distances:

    def __init__(self):
        pass

    def euclidian(self, X_train, input_X):
        """Compute the ecuclidean distance between X_train and input_X
        """
        differences_matrix = np.tile(input_X, (X_train.shape[0], 1)) - X_train
        squared_differences_matrix = differences_matrix**2
        squared_distances_matrix = squared_differences_matrix.sum(axis=1)
        return squared_distances_matrix ** 0.5

    def manhattan(self, X_train, input_X):
        """Compute the manhattan distance between X_train and input_X
        """
        differences_matrix = np.tile(input_X, (X_train.shape[0], 1)) - X_train
        differences_matrix = np.abs(differences_matrix)
        distances_matrix = differences_matrix.sum(axis=1)
        return distances_matrix

    def canberra(self, X_train, input_X):
        """Compute the canberra distance between X_train and input_X
        """
        differences_matrix = np.tile(input_X, (X_train.shape[0], 1)) - X_train
        differences_matrix = np.abs(differences_matrix)
        summations_matrix = np.tile(input_X, (X_train.shape[0], 1)) + X_train
        summations_matrix = np.abs(summations_matrix)
        divisions_matrix = differences_matrix / summations_matrix
        distances_matrix = divisions_matrix.sum(axis=1)
        return distances_matrix

    def chebychev(self, X_train, input_X):
        """Compute the chebychev distance between X_train and input_X
        """
        distances_matrix = np.tile(input_X, (X_train.shape[0], 1)) - X_train
        distances_matrix = np.abs(distances_matrix)
        distances_matrix = distances_matrix.max(axis=1)
        return distances_matrix


import numpy
import matplotlib
from matplotlib import pyplot
from scipy.optimize import fsolve

matplotlib.rcParams.update({'font.size':18, 'figure.figsize':(10,6)})

def relax_dirichlet(p, q, f, interval, bcs, N):
    x, dx = numpy.linspace(interval[0], interval[1], N+2, retstep=True)
    x_interior = x[1:-1]
    A = numpy.zeros((N,N))
    F = numpy.zeros((N,))
    y = numpy.zeros((N+2,))
    for i in range(N):
        A[i,i] = dx**2 * q(x_interior[i]) - 2
        if i>0:
            A[i,i-1] = 1 - dx/2 * p(x_interior[i])
        if i<N-1:
            A[i,i+1] = 1 + dx/2 * p(x_interior[i])
        F[i] = dx**2 * f(x_interior[i])
    F[0] = F[0] - bcs[0] * (1 - dx/2 * p(x_interior[0]))
    F[-1] = F[-1] - bcs[-1] * (1 + dx/2 * p(x_interior[-1]))
    y_interior = numpy.linalg.solve(A, F)
    y[0] = bcs[0]
    y[1:-1] = y_interior
    y[-1] = bcs[-1]

    return x, y

def relax_blackbox(f, bcs, N):
    x, dx = numpy.linspace(0, 1, N+2, retstep=True)
    def residual(y):
        y[0] = bcs[0]
        y[-1] = bcs[-1]
        dy = (y[2:] - y[:-2]) / (2*dx)
        res = numpy.zeros_like(y)
        res[1:-1] = y[:-2] + y[2:] - 2*y[1:-1] - dx**2 * f(x[1:-1], y[1:-1], dy)
        return res
    y_initial = numpy.zeros_like(x)
    y = fsolve(residual, y_initial)
    return x, y

def relax_newton(f, dfdy, dfddy, bcs, N):
    x, dx = numpy.linspace(0, 1, N+2, retstep=True)
    y = numpy.zeros_like(x)
    y_old = numpy.ones_like(x)
    step = 0
    while numpy.linalg.norm(y-y_old) > 1e-10 and step < 100:
        y_old = y.copy()
        step = step + 1
        y[0] = bcs[0]
        y[-1] = bcs[-1]
        x_interior = x[1:-1]
        y_interior = y[1:-1]
        dy = (y[2:] - y[:-2]) / (2*dx)
        residual = y[:-2] + y[2:] - 2*y[1:-1] - dx**2 * f(x[1:-1], y[1:-1], dy)
        J = numpy.zeros((N,N))
        for i in range(N):
            J[i,i] = -2 - dx**2*dfdy(x_interior[i], y_interior[i], dy[i])
            if i>0:
                J[i,i-1] = 1 + dx/2*dfddy(x_interior[i], y_interior[i], dy[i])
            if i<N-1:
                J[i,i+1] = 1 - dx/2*dfddy(x_interior[i], y_interior[i], dy[i])
        y_new_interior = y_interior + numpy.linalg.solve(J, -residual)
        y[1:-1] = y_new_interior

    return x, y

if __name__=="__main__":
    def p(x):
        return numpy.ones_like(x)
    def q(x):
        return numpy.zeros_like(x)
    def f(x):
        return -numpy.ones_like(x)

    x_exact = numpy.linspace(0, 1, 1000)
    x, y = relax_dirichlet(p, q, f, [0, 1], [0, 1], 5)
    pyplot.plot(x, y, 'kx', mew=2)
    pyplot.plot(x_exact, 2*numpy.exp(1)/(numpy.exp(1)-1)*(1-numpy.exp(-x_exact))-x_exact)
    pyplot.xlabel(r"$x$")
    pyplot.show()
    pyplot.plot(x, y-(2*numpy.exp(1)/(numpy.exp(1)-1)*(1-numpy.exp(-x))-x))
    pyplot.xlabel(r"$x$")
    pyplot.show()

    def f_nonlinear(x, y, dy):
        return -1/(1+y**2)
    def dfdy_nonlinear(x, y, dy):
        return 2*y/(1+y**2)**2
    def dfddy_nonlinear(x, y, dy):
        return numpy.zeros_like(x)

    x, y = relax_blackbox(f_nonlinear, [0, 0], 50)
    pyplot.plot(x, y, 'k--')
    pyplot.show()

    x, y = relax_newton(f_nonlinear, dfdy_nonlinear, dfddy_nonlinear,
                        [0, 0], 50)
    pyplot.plot(x, y, 'k--')
    pyplot.show()


import numpy as np

class NaiveBayesClassifier():
    def __init__(self, caso = 1):
        self.mean = None
        self.variance = None
        self.probs = None
        self.ys = None
        self.caso = caso

    def train(self, X, y):
        y_unique = np.unique(y)
        mean = []
        variance = []
        probs = []
        ys = []

        for _y in y_unique:
            j = 0
            new_X = []
            for i in range(len(X)):
                if y[i] == _y:
                    new_X.append(X[i])
                    j += 1
            mean.append(np.mean(np.array(new_X).T, axis = 1))
            variance.append(np.cov(np.array(new_X).T))
            ys.append(_y)
            probs.append(j / len(y))

        self.mean = mean
        self.variance = variance
        self.probs = probs
        self.ys = ys

    def print_params(self):
        for i in range(len(self.ys)):
            print("Y {} {:.2f}%".format(self.ys[i], self.probs[i]*100.0))
            print("MÉDIA")
            print(self.mean[i])
            print("VARIANCIA")
            print(self.variance[i])
            print("\n")

    def predict(self, X):
        y = []
        for x in X:
            dj = []
            for i in range(len(self.ys)):
                if self.caso == 1:
                    t1 = np.log(self.probs[i])

                    det = np.linalg.det(self.variance[i])
                    t2 = - 1/2 * np.log(det)

                    tsub = np.subtract(x, self.mean[i])
                    t3 = np.matmul(tsub.T, np.linalg.pinv(self.variance[i]))
                    t4 = -1/2 * np.matmul(t3, tsub)

                    dj.append(t1 + t2 + t4)

                elif self.caso == 2:
                    det = np.linalg.det(self.variance[i])
                    t2 = - 1/2 * np.log(det)

                    tsub = np.subtract(x, self.mean[i])
                    t3 = np.matmul(tsub.T, np.linalg.pinv(self.variance[i]))
                    t4 = -1/2 * np.matmul(t3, tsub)

                    dj.append(t2 + t4)

                else:
                    tsub = np.subtract(x, self.mean[i])
                    t3 = np.matmul(tsub.T, np.linalg.pinv(self.variance[i]))
                    t4 = -1/2 * np.matmul(t3, tsub)

                    dj.append(t4)
            #print(dj)
            y.append(self.ys[np.argmax(dj)])

        return y

    def score(self, X, true_y):
        predict_y = self.predict(X)
        total = len(predict_y)
        certos = 0
        for i in range(total):
            if predict_y[i] == true_y[i]:
                certos += 1
        return certos/total

    def confusion_matrix(self, X, true_y):
        predict_y = self.predict(X)
        cm = np.zeros((len(self.ys), len(self.ys)), int)
        for i in range(len(predict_y)):
            #print("{} -> {}x{}".format(i, predict_y[i], true_y[i]))
            cm[predict_y[i] - 1, true_y[i] - 1] += 1
        return cm


#!/usr/bin/env python
# coding: utf-8

# # Gaussian Code Exercise
#
# Read through the code below and fill out the TODOs. You'll find a cell at the end of the Jupyter notebook containing unit tests. After you've run the code cell with the Gaussian class, you can run the final cell to check that your code functions as expected.
#
# This exercise includes a file called 'numbers.txt', which you can see if you click on the 'Jupyter' icon at the top of the workspace and then go into the folder titled 3.OOP_code_gaussian_class. The 'numbers.txt' file is read in by the read_data_file() method. There is also a solution in the 3.OOP_code_gaussian_class folder in a file called answer.py.

# In[50]:

import math
import matplotlib.pyplot as plt

class Gaussian():
    """ Gaussian distribution class for calculating and
    visualizing a Gaussian distribution.

    Attributes:
        mean (float) representing the mean value of the distribution
        stdev (float) representing the standard deviation of the distribution
        data_list (list of floats) a list of floats extracted from the data file

    """
    def __init__(self, mu = 0, sigma = 1):

        self.mean = mu
        self.stdev = sigma
        self.data = []

    def calculate_mean(self):
        print("calculate_mean()")

        """Method to calculate the mean of the data set.

        Args:
            None

        Returns:
            float: mean of the data set

        """

        #TODO: Calculate the mean of the data set. Remember that the data set is stored in self.data
        # Change the value of the mean attribute to be the mean of the data set
        # Return the mean of the data set
        ttl = 0.0
        qty = 0
        for val in self.data:
           ttl += val
           qty += 1
        #print("ttl {}".format(ttl))
        #print("qty {}".format(qty))
        if 0 < qty and 0 < ttl:
            return ttl / qty
        else:
            return 0

    def calculate_stdev(self, sample=True):
        print("calculate_stdev()")
        """Method to calculate the standard deviation of the data set.

        Args:
            sample (bool): whether the data represents a sample or population

        Returns:
            float: standard deviation of the data set

        """

        # TODO:
        #   Calculate the standard deviation of the data set
        #
        #   The sample variable determines if the data set contains a sample or a population
        #   If sample = True, this means the data is a sample.
        #   Keep the value of sample in mind for calculating the standard deviation
        #
        #   Make sure to update self.stdev and return the standard deviation as well

        # https://www.khanacademy.org/math/probability/data-distributions-a1/summarizing-spread-distributions/a/calculating-standard-deviation-step-by-step
        # Step 1: Find the mean.
        m = self.calculate_mean()
        # Step 2: For each data point, find the square of its distance to the mean.
        # Step 3: Sum the values from Step 2.
        sum_p = 0.0
        qty = 0
        for p in self.data:
            sum_p += (p - m)**2
            qty += 1
        # Step 4: Divide by the number of data points.
        print("sum_p {}".format(sum_p))
        print("qty {}".format(qty))
        val = 0
        if 0 < qty:
            val = sum_p / qty
        # Step 5: Take the square root.
            self.stdev = math.sqrt(val)
        return self.stdev

    def read_data_file(self, file_name, sample=True):

        """Method to read in data from a txt file. The txt file should have
        one number (float) per line. The numbers are stored in the data attribute.
        After reading in the file, the mean and standard deviation are calculated

        Args:
            file_name (string): name of a file to read from

        Returns:
            None

        """
        data_list = []
        # This code opens a data file and appends the data to a list called data_list
        with open(file_name) as file:
            data_list = []
            line = file.readline()
            while line:
                data_list.append(int(line))
                line = file.readline()
        file.close()

        # TODO:
        #   Update the self.data attribute with the data_list
        #   Update self.mean with the mean of the data_list.
        #       You can use the calculate_mean() method with self.calculate_mean()
        #   Update self.stdev with the standard deviation of the data_list. Use the
        #       calcaulte_stdev() method.
        self.data = []
        for val in data_list:
            #print(float(val))
            self.data.append(val)
        self.mean = self.calculate_mean()
        print("self.mean {}".format(self.mean))
        self.stdev = self.calculate_stdev(False)
        print("self.stdev {}".format(self.stdev))

    import numpy as np
    import matplotlib.mlab as mlab
    import matplotlib.pyplot as plt
    def plot_histogram(self):
        """Method to output a histogram of the instance variable data using
        matplotlib pyplot library.

        Args:
            None

        Returns:
            None
        """

        # TODO: Plot a histogram of the data_list using the matplotlib package.
        #       Be sure to label the x and y axes and also give the chart a title
        num_bins = len(self.data)
        n, bins, patches = plt.hist(self.data, num_bins, facecolor='blue', alpha=0.5)
        plt.show()

    def pdf(self, x):
        """Probability density function calculator for the gaussian distribution.

        Args:
            x (float): point for calculating the probability density function

        Returns:
            float: probability density function output
        """

        # TODO: Calculate the probability density function of the Gaussian distribution
        #       at the value x. You'll need to use self.stdev and self.mean to do the calculation
        pass

    def plot_histogram_pdf(self, n_spaces = 50):

        """Method to plot the normalized histogram of the data and a plot of the
        probability density function along the same range

        Args:
            n_spaces (int): number of data points

        Returns:
            list: x values for the pdf plot
            list: y values for the pdf plot

        """

        #TODO: Nothing to do for this method. Try it out and see how it works.

        mu = self.mean
        sigma = self.stdev

        min_range = min(self.data)
        max_range = max(self.data)

         # calculates the interval between x values
        interval = 1.0 * (max_range - min_range) / n_spaces

        x = []
        y = []

        # calculate the x values to visualize
        for i in range(n_spaces):
            tmp = min_range + interval*i
            x.append(tmp)
            y.append(self.pdf(tmp))

        # make the plots
        fig, axes = plt.subplots(2,sharex=True)
        fig.subplots_adjust(hspace=.5)
        axes[0].hist(self.data, density=True)
        axes[0].set_title('Normed Histogram of Data')
        axes[0].set_ylabel('Density')

        axes[1].plot(x, y)
        axes[1].set_title('Normal Distribution for \n Sample Mean and Sample Standard Deviation')
        axes[0].set_ylabel('Density')
        plt.show()

        return x, y

# In[51]:

# Unit tests to check your solution

import unittest

class TestGaussianClass(unittest.TestCase):
    def setUp(self):
        self.gaussian = Gaussian(25, 2)

    def test_initialization(self):
        self.assertEqual(self.gaussian.mean, 25, 'incorrect mean')
        self.assertEqual(self.gaussian.stdev, 2, 'incorrect standard deviation')

    #def test_pdf(self):
    #    self.assertEqual(round(self.gaussian.pdf(25), 5), 0.19947,\
    #     'pdf function does not give expected result')

    def test_meancalculation(self):
        self.gaussian.read_data_file('numbers.txt', True)
        self.assertEqual(self.gaussian.calculate_mean(),         sum(self.gaussian.data) / float(len(self.gaussian.data)), 'calculated mean not as expected')

    def test_stdevcalculation(self):
        self.gaussian.read_data_file('numbers.txt', True)
        self.assertEqual(round(self.gaussian.stdev, 2), 92.87, 'sample standard deviation incorrect')
        self.gaussian.read_data_file('numbers.txt', False)
        self.assertEqual(round(self.gaussian.stdev, 2), 88.55, 'population standard deviation incorrect')

tests = TestGaussianClass()

tests_loaded = unittest.TestLoader().loadTestsFromModule(tests)

unittest.TextTestRunner().run(tests_loaded)

# In[ ]:

# In[ ]:


import numpy as np

def normalize(tensor, min_orig, max_orig, min_out=-1., max_out=1.):
    delta = max_out - min_out
    return delta * (tensor - min_orig) / (max_orig - min_orig) + min_out

def min_max_data_np(arrays):
    all_max = []
    all_min = []

    for arr in arrays:
        all_min.append(np.min(arr))
        all_max.append(np.max(arr))

    data_min = np.min(all_min)
    data_max = np.max(all_max)

    return data_min, data_max


import math
import random

from numpy.linalg import inv
from numpy.linalg import det
from scipy.stats import norm

class Point:
  def __init__(self, pos, meta = None):
    self.pos = pos
    self.meta = meta
  def __iter__(self):
    for i in self.pos:
      yield i

## Euclidean distance between two points
def eucl_distance(a, b):
  a = tuple(a)
  b = tuple(b)
  total = 0;
  for i in range(0, len(a)):
    total += (a[i] - b[i]) ** 2
  return math.sqrt(total)

## Sum of absolute difference
def sad_distance(a, b):
  a = tuple(a)
  b = tuple(b)
  total = 0;
  for i in range(0, len(a)):
    total += abs(a[i] - b[i])
  return total

## Sum of squared differences
## aka euclidean norm
def ssd_distance(a, b):
  a = tuple(a)
  b = tuple(b)
  total = 0;
  for i in range(0, len(a)):
    total += (a[i] - b[i]) ** 2
  return total

## Sum of squared differences
## aka euclidean norm
def chebyshev_distance(a, b):
  a = tuple(a)
  b = tuple(b)
  diffs = [];
  for i in range(0, len(a)):
    diffs.append(abs(a[i] - b[i]))
  return max(diffs)

def first(items):
  return items[0] if items else None

def last(items):
  return items[-1] if items else None

## Find an item nearest to the predicate
def nearest(items, predicate, as_index = False, distance_fn = eucl_distance):
  result = None
  min_dist = math.inf
  for i, item in enumerate(items):
    cur_dist = distance_fn(item, predicate)
    if (cur_dist < min_dist):
      min_dist = cur_dist
      result = i if as_index else item
  return result

def tuple_apply(fn, a, b):
  size = len(a)
  result = [None] * size
  for i in range(size):
    result[i] = fn(a[i], b[i])
  return tuple(result)

def mean(items):
  return tuple(sum(x) / len(items) for x in zip(*items))

## TODO: support for Point class
def std(items):
  m = mean(items)
  size = len(m)
  s = [0] * size
  n = len(items)
  for x in items:
    x = tuple(x)
    for i in range(0, size):
      s[i] += (x[i] - m[i]) ** 2
  for i in range(0, size):
    s[i] = math.sqrt(s[i] / n)
  return tuple(s)

def mean_normalize(items):
  x_mean = mean(items)
  return [tuple_apply(lambda a, b: a - b, x, x_mean) for x in items]

def find_two_farthest(items):
  length = len(items)
  max_dist = 0
  result = None
  for i in range(0, length - 1):
    for j in range(i, length):
      a = items[i]
      b = items[j]
      cur_dist = distance(a, b)
      if (cur_dist > max_dist):
        max_dist = cur_dist
        result = (a, b)
  return result

## KMeans
def kmeans(points, k = 2, epsilon = 0.001, iterations = 1000, distance_fn = eucl_distance):
  ## Initialize cluster centers with a random sample
  cluster_centers = random.sample(points, k)

  ## Iterate
  for it in range(iterations):
    ## Assign points to clusters
    clusters = [[] for _ in range(k)]
    for p, point in enumerate(points):
      c = nearest(cluster_centers, point, as_index = True, distance_fn = distance_fn)
      clusters[c].append(point)

    ## Calculate new cluster centers
    cluster_centers_new = [None] * k
    for c, cluster_points in enumerate(clusters):
      cluster_centers_new[c] = mean(cluster_points)

    ## Evaluate difference between new and old cluster centers
    cluster_center_distances = [distance_fn(a, b)
      for a, b in zip(cluster_centers, cluster_centers_new)]

    print('iteration: {}, diff: {}'.format(it, cluster_center_distances))

    ## Break loop when change is smaller than epsilon
    if max(cluster_center_distances) < epsilon:
      break

    ## Replace cluster centers with newer and continue iteration
    cluster_centers = cluster_centers_new

  return cluster_centers, clusters

def gaussian_pdf(point, mu, sigma, lam):
  point = tuple(point)
  n = len(point)
  probability = lam
  for i in range(n):
    probability *= norm.pdf(point[i], mu[i], sigma[i][i])
  return probability

## Expectation Minimization
def em_gaussian(points, k = 2, epsilon = 0.001, iterations = 1000):
  params = {
    'mu': [tuple(x) for x in random.sample(points, k)],
    'sig': [[[1, 0], [0, 1]] for _ in range(k)],
    'lambda': [1/k for _ in range(k)],
  }
  count = len(points)

  ## Iterate
  for it in range(iterations):
    clusters = [[] for _ in range(k)]

    ## Expectation
    for point in points:
      max_prob = 0
      max_prob_cluster = None
      for i in range(k):
        prob = gaussian_pdf(point, params['mu'][i], params['sig'][i], params['lambda'][i])
        if prob > max_prob:
          max_prob = prob
          max_prob_cluster = i
      clusters[max_prob_cluster].append(point)

    ## Maximization
    params_new = {}
    params_new['lambda'] = [len(clusters[i]) / float(count) for i in range(k)]
    params_new['mu'] = [mean(clusters[i]) for i in range(k)]
    params_new['sig'] = []
    for i in range(k):
      cluster_std = std(clusters[i])
      cluster_std = [[cluster_std[0], 0], [0, cluster_std[1]]]
      params_new['sig'].append(cluster_std)

    print(params_new)

    ## Evaluate difference between new and old params
    diff = 0
    for i in range(k):
      diff += (params['mu'][i][0] - params_new['mu'][i][0]) ** 2
      diff += (params['mu'][i][1] - params_new['mu'][i][1]) ** 2
    diff = diff ** 0.5

    ## Break loop when change is smaller than epsilon
    if diff < epsilon:
      break

    params = params_new

    print("iteration {}, diff {}".format(it, diff))

  return [], clusters