from math import sqrt
import numpy as np

def read_KNN_dataFile(file):
    """
    A function to read data for KNN dataset provided.
    """
    A = np.genfromtxt(file)
    return A

def euclidean_distance(p1, p2):
    """
    Returns the Euclidean Distance of a particular point from rest of the points in dataset.
    """
    distance = 0
    for i in range(len(p1)-1):
        distance += (p1[i]-p2[i])**(2)
    return sqrt(distance)

def block_distance(p1, p2):
    """
    Returns the Block Distance of a particular point from rest of the points in dataset.
    """
    distance = 0
    for i in range(len(p1)-1):
        distance += abs(p1[i]-p2[i])
    return distance

def get_neighbours(train, test_row, num_neighbours, distance_metrics="block"):
    """
    Returns n nearest neighbours of a particular point in dataset based on euclidean or block distance.
    """
    distances = []
    for train_row in train:
        if distance_metrics=="block":
            distance = block_distance(test_row, train_row)
        else:
            distance = euclidean_distance(test_row, train_row)
        distances.append((train_row, distance))
    distances.sort(key=lambda tup: tup[1])
    neigbours = []
    for i in range(num_neighbours):
        neigbours.append(distances[i][0])
    return neigbours


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

# In[1]:

import numpy as np
import skimage.transform
import scipy
from scipy import ndimage
import matplotlib.pyplot as plt
from skimage.morphology import medial_axis, watershed

create_dist_map = lambda img, mask=None: medial_axis(img, mask, return_distance=True)[1]
import os
from skimage.measure import block_reduce

plt_settings = {"interpolation": "none"}

# # Distance Maps
# Here we calculate distance maps with the ```ndimage.distance_transform_``` family of functions. Initially we focus on test images since it is easier to see what is happening with these images.

# In[2]:

def generate_dot_image(size=100, cutoff=0.15):
    """
    Create a simple  synthetic image with a repeating pattern
    Keyword arguments:
    size -- the size of the image on one size, final size is size x size (default 100)
    imag -- the cutoff between 0 and 1, higher means less connected objects (default 0.15)
    """
    xx, yy = np.meshgrid(range(size), range(size))
    return (
        np.sin(6 * np.pi * xx / (100) - 1) + 1.25 * np.cos(5 * np.pi * yy / (100) - 2)
        > cutoff
    )

# In[3]:

img_bw = generate_dot_image(28, 0.50)
plt.imshow(img_bw, cmap="gray", **plt_settings)

# In[4]:

img_dist = ndimage.distance_transform_edt(img_bw)
plt.imshow(img_dist, **plt_settings)

# ## Comparing
# There are a number of different methods for ```distance_transform``` inside the ```ndimage``` package of ```scipy``` compare the results of the different approaches for this and other images.
# - What are the main differences?
# - Quantitatively (histogram) show what situations each one might be best suited for?

# In[5]:

# calculate new distance transforms
img_dist = ndimage.distance_transform_edt(img_bw)
img_dist_cityblock = ndimage.distance_transform_cdt(img_bw, metric="taxicab")
img_dist_chess = ndimage.distance_transform_cdt(img_bw, metric="chessboard")

fig, (ax1, ax2, ax3, ax4) = plt.subplots(1, 4, figsize=(30, 10))
ax1.imshow(img_bw, cmap="gray", **plt_settings)
ax1.set_title("Mask Image")
dmap_im = ax2.imshow(img_dist, vmax=img_dist.max(), **plt_settings)
ax2.set_title("Euclidean")
ax3.imshow(img_dist_cityblock, vmax=img_dist.max(), **plt_settings)
ax3.set_title("Cityblock")
ax4.imshow(img_dist_chess, vmax=img_dist.max(), **plt_settings)
ax4.set_title("Chess")
fig.subplots_adjust(right=0.8)
cbar_ax = fig.add_axes([0.85, 0.15, 0.05, 0.7])
cbar = fig.colorbar(dmap_im, cax=cbar_ax)
cbar_ax.set_title("Distance\n(px)")

# ## More Complicated Objects
# We now make the image bigger (changing the ```size``` parameter) and connect them together (the ```cutoff``` parameter)

# In[6]:

# use a bigger base image
img_bw = generate_dot_image(100, 0.15)
img_dist = ndimage.distance_transform_edt(img_bw)
img_dist_cityblock = ndimage.distance_transform_cdt(img_bw, metric="taxicab")
img_dist_chess = ndimage.distance_transform_cdt(img_bw, metric="chessboard")

fig, (ax1, ax2, ax3, ax4) = plt.subplots(1, 4, figsize=(30, 10))
ax1.imshow(img_bw, cmap="gray", **plt_settings)
ax1.set_title("Mask Image")
dmap_im = ax2.imshow(img_dist, vmax=img_dist.max(), **plt_settings)
ax2.set_title("Euclidean")
ax3.imshow(img_dist_cityblock, vmax=img_dist.max(), **plt_settings)
ax3.set_title("Cityblock")
ax4.imshow(img_dist_chess, vmax=img_dist.max(), **plt_settings)
ax4.set_title("Chess")
fig.subplots_adjust(right=0.8)
cbar_ax = fig.add_axes([0.85, 0.15, 0.05, 0.7])
cbar = fig.colorbar(dmap_im, cax=cbar_ax)
cbar_ax.set_title("Distance\n(px)")

# # Watershed
# We can use the watershed transform to segment closely connected objects. We see in the first image that the standard connected component labeling ```ndimage.label``` shows only 3 when we see 9

# In[7]:

cc_img = ndimage.label(img_bw)[0]

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(30, 10))
ax1.imshow(img_bw, cmap="gray", **plt_settings)
ax1.set_title("Mask Image")
dmap_im = ax2.imshow(cc_img, **plt_settings)
ax2.set_title("Connected Component Analysis")

# In[8]:

from skimage.feature import peak_local_max

def simple_watershed(img_dist, img_bw):
    """
    Calculate the watershed transform on an image and its distance map
    by finding the troughs and expanding from these points
    """
    local_maxi = peak_local_max(
        img_dist, labels=img_bw, footprint=np.ones((3, 3)), indices=False
    )
    markers = ndimage.label(local_maxi)[0]
    return watershed(-img_dist, markers, mask=img_bw)

# ## Applying Watershed
# We can apply watershed to the following image.
# - Why do the bottom row of objects not show up?
# - How can the results be improved

# In[9]:

ws_img = simple_watershed(img_dist, img_bw)

fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(30, 10))
ax1.imshow(img_bw, cmap="gray", **plt_settings)
ax1.set_title("Mask Image")
ax2.imshow(cc_img, **plt_settings)
ax2.set_title("Connected Component Analysis")
ax3.imshow(ws_img, **plt_settings)
ax3.set_title("Watershed Analysis")

# In[ ]:


from typing import Union, Optional, Dict
import numpy as np
from scipy import stats

def hist_entropy(
    X: np.ndarray,
    bins: Union[str, int] = "auto",
    correction: bool = True,
    hist_kwargs: Optional[Dict] = {},
) -> float:
    """Calculates the entropy using the histogram of a univariate dataset.
    Option to do a Miller Maddow correction.

    Parameters
    ----------
    X : np.ndarray, (n_samples)
        the univariate input dataset

    bins : {str, int}, default='auto'
        the number of bins to use for the histogram estimation

    correction : bool, default=True
        implements the Miller-Maddow correction for the histogram
        entropy estimation.

    hist_kwargs: Optional[Dict], default={}
        the histogram kwargs to be used when constructing the histogram
        See documention for more details:
        https://docs.scipy.org/doc/numpy/reference/generated/numpy.histogram.html

    Returns
    -------
    H_hist_entropy : float
        the entropy for this univariate histogram

    Example
    -------
    >> from scipy import stats
    >> from pysim.information import histogram_entropy
    >> X = stats.gamma(a=10).rvs(1_000, random_state=123)
    >> histogram_entropy(X)
    array(2.52771628)
    """
    # get histogram
    hist_counts = np.histogram(X, bins=bins, **hist_kwargs)

    # create random variable
    hist_dist = stats.rv_histogram(hist_counts)

    # calculate entropy
    H = hist_dist.entropy()

    # MLE Estimator with Miller-Maddow Correction
    if correction == True:
        H += 0.5 * (np.sum(hist_counts[0] > 0) - 1) / hist_counts[0].sum()

    return H


# -*- coding: utf-8 -*-
"""
Provides euclidean functions for working with TSP data.

One key function converts a numpy array of 2D coordinates into a matrix of euclidean distances
between them.
"""

import numpy as np

def gen_matrix(cities):
    """
    Creates a numpy array of euclidian distances between 2 sets of
    cities

    @points = numpy arrange of coordinate pairs

    """
    size = len(cities)
    matrix = np.zeros(shape=(size, size))

    row = 0
    col = 0

    for city1 in cities:
        col = 0
        for city2 in cities:
            matrix[row, col] = euclidean_distance(city1, city2)
            col+=1
        row +=1

    return matrix

def euclidean_distance(city1, city2):
    """
    Calculate euc distance between 2 cities
    5.5 ms to execute
    """
    return np.linalg.norm((city1-city2))

def euclidean_distance2(city1, city2):
    """
    An alterantive way to calculate euc distance
    between two cities

    6-7 ms to execute
    """
    v = np.square(city1 - city2)
    return np.sqrt(np.sum(v))


''' Python question by HackerRank
TODO 1:
You are given a 2-D array with dimensions NxM.
Your task is to perform the  tool over axis 0 and then find the Product of that result.
Input Format:
The first line of input contains space separated values of N and M.
The next N lines contains M space separated integers.
Output Format: Compute the sum along axis 0. Then, print the product of that sum.

TODO 2:
You are given a 2-D array with dimensions NxM.
Your task is to perform the min function over axis 1 and then find the max of that.
Input Format:
The first line of input contains the space separated values of N and M.
The next N lines contains M space separated integers.
Output Format: Compute the min along axis 1 and then print the max of that result.

TODO 3:
You are given a 2-D array of size NxM.
Your task is to find:
The mean along axis 1
The var along axis 0
The std along axis None

Input Format:
The first line contains the space separated values of N and M.
The next N lines contains M space separated integers.

Output Format
First, print the mean.
Second, print the var.
Third, print the std.

TODO 4:
You are given two arrays A and B. Both have dimensions of NxN.
Your task is to compute their matrix product.
Input Format:
The first line contains the integer N.
The next N lines contains  space separated integers of array A.
The following N lines contains N space separated integers of array B.
Output Format: Print the matrix multiplication of A and B.

TODO 5:
You are given two arrays: A and B.
Your task is to compute their inner and outer product.
Input Format
The first line contains the space separated elements of array .
The second line contains the space separated elements of array .
Output Format:
First, print the inner product.
Second, print the outer product.
'''

import numpy
import sys

def get_array_from_input():
    array_size = tuple([int(item) for item in input().split()])
    array_list = []
    for i in range(array_size[0]):
        array_list.append(tuple([int(item) for item in input().split()]))

    new_array = numpy.array(array_list)
    return new_array

#TODO 1: "Sum and Prod" (Numpy)
def sum_and_prod():
    # array_size = tuple([int(item) for item in input().split()])
    # array_list = []
    # for i in range(array_size[0]):
    #     array_list.append(tuple([int(item) for item in input().split()]))

    # my_array = numpy.array(array_list)
    my_array = get_array_from_input()
    step_1 = numpy.sum(my_array, axis = 0)
    print (step_1)
    print(numpy.prod(step_1))
    return 0

#TODO 2: "Min and Max" (Numpy)
def min_and_max():
    my_array = get_array_from_input()
    step_1 = numpy.min(my_array, axis=1)
    print(numpy.max(step_1))
    return 0

#TODO 3: "Mean, Var, and Std"
def mean_var_std():
    my_array = get_array_from_input()
    print(numpy.mean(my_array, axis=1))
    print(numpy.var(my_array, axis=0))
    print(numpy.std(my_array))
    return 0

#TODO 4: "Dot and Cross"
def dot_cross():
    array_size = int(input())
    array_list = []
    for i in range(array_size):
        array_list.append(tuple([int(item) for item in input().split()]))
    array_A = numpy.array(array_list)
    array_list.clear()
    for i in range(array_size):
        array_list.append(tuple([int(item) for item in input().split()]))
    array_B = numpy.array(array_list)

    print(numpy.dot(array_A, array_B))

    return 0

#TODO 5: "Inner_Outer"
def inner_outer():
    array_A = numpy.array([int(item) for item in input().split()])
    array_B = numpy.array([int(item) for item in input().split()])
    print(numpy.inner(array_A, array_B))
    print(numpy.outer(array_A, array_B))

def main():
    # sum_and_prod()
    # min_and_max()
    # mean_var_std()
    # dot_cross()
    inner_outer()

if __name__ == '__main__':
    main()


import numpy as np

def divisors(x: int):
    """
    Returns the divisors of x, up to the square root of x.
    """
    divs = []
    upperbound = int(np.round(np.sqrt(x)))

    for i in range(1, upperbound + 1):
        if x % i == 0:
            divs.append(i)
    return divs

def n_rows_cols(x: int, squared: bool=False):
    """
    Returns the 'squarest' divisors of x.

    The 'squarest' divisors of x are defined as the divisors of x that are
    closest together on the number line.

    Prime numbers are automatically assigned to the next largest square.

    :param x: The integer to split into rows and columns.
    :param squared: Whether to go for a squarer layout or not.
    """
    divs = divisors(x)
    isprime = len(divs) == 1 and divs[0] == 1
    if squared or isprime:
        nrows = int(np.ceil(np.sqrt(x)))
        ncols = nrows
    else:
        nrows = max(divisors(x))
        ncols = int(x / nrows)
    return nrows, ncols


import numpy
import pandas

def compute_cost(features, values, theta):
    """
    Compute the cost of a list of parameters, theta, given a list of features
    (input data points) and values (output data points).
    """
    m = len(values)
    sum_of_square_errors = numpy.square(numpy.dot(features, theta) - values).sum()
    cost = sum_of_square_errors / (2*m)

    return cost

def gradient_descent(features, values, theta, alpha, num_iterations):
    """
    Perform gradient descent given a data set with an arbitrary number of features.
    """

    # Write code here that performs num_iterations updates to the elements of theta.
    # times. Every time you compute the cost for a given list of thetas, append it
    # to cost_history.
    # See the Instructor notes for hints.

    cost_history = []
    m = len(values)

    ###########################
    ### YOUR CODE GOES HERE ###
    ###########################
    for i in range (0,num_iterations):
        predicted_value = numpy.dot(features,theta)
        theta = theta + alpha / m * numpy.dot(values-predicted_value,features)
        cost_history.append(compute_cost(features, values, theta))

    return theta, pandas.Series(cost_history) # leave this line for the grader


from typing import List

import numpy as np

def init_state(list_of_fish: List[int]) -> np.ndarray:
    # only store number of each counter value
    state = np.zeros(9, dtype=np.int64)  # int64 to avoid overflow star 2
    for fish in list_of_fish:
        state[fish] += 1
    return state

def simulate(state: np.ndarray, n_days: int)-> int:
    for _ in range(n_days):
        # spawning fish
        n_zeros = state[0]
        # decrement all counters
        state[0:8] = state[1:9]
        # spawn new fish
        state[8] = n_zeros
        # reset spawning fish
        state[6] += n_zeros
    return np.sum(state)

if __name__ == '__main__':
    def _main():
        with open('../inputs/day6.txt', 'r') as f:
            inp = f.readline().strip().split(',')
        inp = [int(s) for s in inp]

        print(f"Star 1: {simulate(init_state(inp), 80)}")
        print(f"Star 2: {simulate(init_state(inp), 256)}")

    _main()


def verify_1a(answer):

    import sympy

    x = sympy.Symbol('x')
    y = sympy.Symbol('y')

    return answer == sympy.Matrix([x,y])

def verify_2a(answer):

    import sympy

    q = sympy.Symbol('theta', positive=True)# Rotation angle
    b = sympy.Matrix([1,0])
    R = sympy.Matrix([[sympy.cos(q), -sympy.sin(q)],[sympy.sin(q), sympy.cos(q)]])

    solution = R*b
    return solution == answer

def verify_3a(answer):

    import sympy

    A = sympy.Matrix([[1,2],[3,4]])
    b = sympy.Matrix([5,6])

    sol = A.inv()*b

    return sol==answer

def verify_4a(answer):

    import sympy

    x = sympy.Symbol('x', positive=True)
    matrix = sympy.Matrix([[1,2-x],[3,4]])
    sol = sympy.solve(matrix.det(),x)[0]
    return answer==sol

def verify_5a(answer):

    import sympy

    M = sympy.Matrix([[-1,1,0],
                      [1,-2,1],
                      [0,1,-1]])
    return answer == M.eigenvects()

def verify_6a(answer):

    import sympy

    psi = sympy.Symbol('psi', positive=True) # Rapidity
    gen = sympy.Matrix([[0,-1],[-1,0]])

    return sympy.exp(psi*gen).simplify() == answer.simplify()


# %%
import math
import copy
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
from scipy.special import expit as sigmoid, logit
# %%
np.random.seed(0)
# %%

def generate_dataset(N_points):
    # 1 class
    radiuses = np.random.uniform(0, 0.5, size=N_points//2)
    angles = np.random.uniform(0, 2*math.pi, size=N_points//2)

    x_1 = np.multiply(radiuses, np.cos(angles)).reshape(N_points//2, 1)
    x_2 = np.multiply(radiuses, np.sin(angles)).reshape(N_points//2, 1)
    X_class_1 = np.concatenate((x_1, x_2), axis=1)
    Y_class_1 = np.full((N_points//2,), 1)

    # 0 class
    radiuses = np.random.uniform(0.6, 1, size=N_points//2)
    angles = np.random.uniform(0, 2*math.pi, size=N_points//2)

    x_1 = np.multiply(radiuses, np.cos(angles)).reshape(N_points//2, 1)
    x_2 = np.multiply(radiuses, np.sin(angles)).reshape(N_points//2, 1)
    X_class_0 = np.concatenate((x_1, x_2), axis=1)
    Y_class_0 = np.full((N_points//2,), 0)

    X = np.concatenate((X_class_1, X_class_0), axis=0)
    Y = np.concatenate((Y_class_1, Y_class_0), axis=0)
    return X, Y

N_points = 1000
X, Y = generate_dataset(N_points)

plt.scatter(X[:N_points//2, 0], X[:N_points//2, 1], color='red', label='class 1')
plt.scatter(X[N_points//2:, 0], X[N_points//2:, 1], color='blue', label='class 0')
plt.legend(loc=9, bbox_to_anchor=(0.5, -0.1), ncol=2)
plt.show()
# %%
X.shape, Y.shape
# %%
weights = dict(
    W1=np.random.randn(3, 2),
    b1=np.zeros(3),
    W2=np.random.randn(3),
    b2=0
)
# weights = {'W1': np.array([[-0.1049797,  1.36741498],
#                            [-1.65534404,  0.15364446],
#                            [-1.58447356,  0.84445431]]),
#            'b1': np.array([0., 0., 0.]),
#            'W2': np.array([-1.21286782,  0.28376955, -0.28219588]),
#            'b2': 0}
initial_weights = copy.deepcopy(weights)

# %%

def forward_propagation(X, weights):
    Z1 = X@weights['W1'].T + weights['b1']
    H = sigmoid(Z1)

    Z2 = H@weights['W2']+weights['b2']
    Y = sigmoid(Z2)

    return Y, Z2, H, Z1

# %%
forward_propagation(X, weights)

# %%

06Deep Learning/01Introduction to Neural Networks/OnlineClass/nn_from_scratch.py


import numpy as np
from pom.calculations import calculate_vertical_density_profile

def powspace(start, stop, power, num):
    start = np.power(start, 1/float(power))
    stop = np.power(stop, 1/float(power))
    return np.power(np.linspace(start, stop, num=num), power)

# ======================================================================================================================
# Test 1 - Constant profile
# Linear spacing with 61 points --> dz = 0.1
# One hour --> timestep = 60 s
# Constant temperature --> T = 20 C
# Constant salinity --> S = 35 psu

print("--------------------------------------------------------------")
print("Test 1")
print()

vertical_layers = 61
timestep = 60
vertical_spacing_staggered = np.linspace(0,6,vertical_layers)
temperature = 20 * np.ones(vertical_layers)
salinity = 35 * np.ones(vertical_layers)

vertical_density_profile = calculate_vertical_density_profile(temperature,salinity,vertical_spacing_staggered,timestep,vertical_layers)
density_profile_spacing = np.zeros(vertical_layers-1)
for i in range(0,vertical_layers-1):
    density_profile_spacing[i] = vertical_density_profile[i+1] - vertical_density_profile[i]
print("Test 1: vertical density profile = \n", vertical_density_profile)
print()
print("Test 1: density profile spacing = \n", density_profile_spacing)
print()

# Result: constant density profile
# ======================================================================================================================

# ======================================================================================================================
# Test 2 - Temperature dependancy
# Linear spacing with 61 points --> dz = 0.1
# One hour --> timestep = 60 s
# Linear increase in temperature --> 10 C < T < 20 C
# Constant salinity --> S = 35 psu

print("--------------------------------------------------------------")
print("Test 2")
print()

vertical_layers = 61
timestep = 60
vertical_spacing_staggered = np.linspace(0,6,vertical_layers)
temperature = np.linspace(10,20,vertical_layers)
temperature_spacing = np.zeros(vertical_layers-1)
for i in range(0,vertical_layers-1):
    temperature_spacing[i] = temperature[i+1] - temperature[i]
print("Test 2: temperature = \n", temperature)
print()
print("Test 2: temperature spacing = \n", temperature_spacing)
print()
salinity = 35 * np.ones(vertical_layers)

vertical_density_profile = calculate_vertical_density_profile(temperature,salinity,vertical_spacing_staggered,timestep,vertical_layers)
density_profile_spacing = np.zeros(vertical_layers-1)
for i in range(0,vertical_layers-1):
    density_profile_spacing[i] = vertical_density_profile[i+1] - vertical_density_profile[i]
print("Test 2: vertical density profile = \n", vertical_density_profile)
print()
print("Test 2: density profile spacing = \n", density_profile_spacing)
print()

# Result: linearly decreasing density with linearly increasing temperature
# Further testing: - lower values of temperature (potentially test negative to positive)
#                  - decreasing temperature profile (ocean temperature decreases with depth)
# ======================================================================================================================

# ======================================================================================================================
# Test 3 - Temperature dependancy
# Linear spacing with 61 points --> dz = 0.1
# One hour --> timestep = 60 s
# Logarithmic increase in temperature --> 10 C < T < 20 C
# Constant salinity --> S = 35 psu

print("--------------------------------------------------------------")
print("Test 3")
print()

vertical_layers = 61
timestep = 60
vertical_spacing_staggered = np.linspace(0,6,vertical_layers)
temperature = np.logspace(1,np.log(20)/np.log(10),vertical_layers)
temperature_spacing = np.zeros(vertical_layers-1)
for i in range(0,vertical_layers-1):
    temperature_spacing[i] = temperature[i+1] - temperature[i]
print("Test 3: temperature = \n", temperature)
print()
print("Test 3: temperature spacing = \n", temperature_spacing)
print()
salinity = 35 * np.ones(vertical_layers)

vertical_density_profile = calculate_vertical_density_profile(temperature,salinity,vertical_spacing_staggered,timestep,vertical_layers)
density_profile_spacing = np.zeros(vertical_layers-1)
for i in range(0,vertical_layers-1):
    density_profile_spacing[i] = vertical_density_profile[i+1] - vertical_density_profile[i]
print("Test 3: vertical density profile = \n", vertical_density_profile)
print()
print("Test 3: density profile spacing = \n", density_profile_spacing)
print()

# Result: decreasing density with increasing temperature
# ======================================================================================================================

# ======================================================================================================================
# Test 4 - Temperature dependancy
# Linear spacing with 61 points --> dz = 0.1
# One hour --> timestep = 60 s
# 10th power increase in temperature --> 10 C < T < 20 C
# Constant salinity --> S = 35 psu

print("--------------------------------------------------------------")
print("Test 4")
print()

vertical_layers = 61
timestep = 60
vertical_spacing_staggered = np.linspace(0,6,vertical_layers)
temperature = powspace(10,20,10,vertical_layers)
temperature_spacing = np.zeros(vertical_layers-1)
for i in range(0,vertical_layers-1):
    temperature_spacing[i] = temperature[i+1] - temperature[i]
print("Test 4: temperature = \n", temperature)
print()
print("Test 4: temperature spacing = \n", temperature_spacing)
print()
salinity = 35 * np.ones(vertical_layers)

vertical_density_profile = calculate_vertical_density_profile(temperature,salinity,vertical_spacing_staggered,timestep,vertical_layers)
density_profile_spacing = np.zeros(vertical_layers-1)
for i in range(0,vertical_layers-1):
    density_profile_spacing[i] = vertical_density_profile[i+1] - vertical_density_profile[i]
print("Test 4: vertical density profile = \n", vertical_density_profile)
print()
print("Test 4: density profile spacing = \n", density_profile_spacing)
print()

# Result: decreasing density with increasing temperature
# ======================================================================================================================

# ======================================================================================================================
# Test 5 - Temperature dependancy
# Linear spacing with 61 points --> dz = 0.1
# One hour --> timestep = 60 s
# Linear Decrease in temperature --> 10 C < T < 20 C
# Constant salinity --> S = 35 psu

print("--------------------------------------------------------------")
print("Test 5")
print()

vertical_layers = 61
timestep = 60
vertical_spacing_staggered = np.linspace(0,6,vertical_layers)
temperature = np.linspace(20,10,vertical_layers)
temperature_spacing = np.zeros(vertical_layers-1)
for i in range(0,vertical_layers-1):
    temperature_spacing[i] = temperature[i+1] - temperature[i]
print("Test 5: temperature = \n", temperature)
print()
print("Test 5: temperature spacing = \n", temperature_spacing)
print()
salinity = 35 * np.ones(vertical_layers)

vertical_density_profile = calculate_vertical_density_profile(temperature,salinity,vertical_spacing_staggered,timestep,vertical_layers)
density_profile_spacing = np.zeros(vertical_layers-1)
for i in range(0,vertical_layers-1):
    density_profile_spacing[i] = vertical_density_profile[i+1] - vertical_density_profile[i]
print("Test 5: vertical density profile = \n", vertical_density_profile)
print()
print("Test 5: density profile spacing = \n", density_profile_spacing)
print()

# Result: increasing density with decreasing temperature
# ======================================================================================================================

# ======================================================================================================================
# Test 6 - Salinity dependancy
# Linear spacing with 61 points --> dz = 0.1
# One hour --> timestep = 60 s
# Constant temperature --> T = 20 C
# Linearly increasing salinity --> 33 psu < S < 37 psu

print("--------------------------------------------------------------")
print("Test 6")
print()

vertical_layers = 61
timestep = 60
vertical_spacing_staggered = np.linspace(0,6,vertical_layers)
temperature = 20 * np.ones(vertical_layers)
salinity = np.linspace(33,37,vertical_layers)
salinity_spacing = np.zeros(vertical_layers-1)
for i in range(0,vertical_layers-1):
    salinity_spacing[i] = salinity[i+1] - salinity[i]
print("Test 6: salinity = \n", salinity)
print()
print("Test 6: salinity spacing = \n", salinity_spacing)
print()

vertical_density_profile = calculate_vertical_density_profile(temperature,salinity,vertical_spacing_staggered,timestep,vertical_layers)
density_profile_spacing = np.zeros(vertical_layers-1)
for i in range(0,vertical_layers-1):
    density_profile_spacing[i] = vertical_density_profile[i+1] - vertical_density_profile[i]
print("Test 6: vertical density profile = \n", vertical_density_profile)
print()
print("Test 6: density profile spacing = \n", density_profile_spacing)
print()

# Result: increasing density with increasing salinity
# ======================================================================================================================

# ======================================================================================================================
# Test 7 - Salinity dependancy
# Linear spacing with 61 points --> dz = 0.1
# One hour --> timestep = 60 s
# Constant temperature --> T = 20 C
# 10th power increase salinity --> 33 psu < S < 37 psu

print("--------------------------------------------------------------")
print("Test 7")
print()

vertical_layers = 61
timestep = 60
vertical_spacing_staggered = np.linspace(0,6,vertical_layers)
temperature = 20 * np.ones(vertical_layers)
salinity = powspace(33,37,10,vertical_layers)
salinity_spacing = np.zeros(vertical_layers-1)
for i in range(0,vertical_layers-1):
    salinity_spacing[i] = salinity[i+1] - salinity[i]
print("Test 7: salinity = \n", salinity)
print()
print("Test 7: salinity spacing = \n", salinity_spacing)
print()

vertical_density_profile = calculate_vertical_density_profile(temperature,salinity,vertical_spacing_staggered,timestep,vertical_layers)
density_profile_spacing = np.zeros(vertical_layers-1)
for i in range(0,vertical_layers-1):
    density_profile_spacing[i] = vertical_density_profile[i+1] - vertical_density_profile[i]
print("Test 7: vertical density profile = \n", vertical_density_profile)
print()
print("Test 7: density profile spacing = \n", density_profile_spacing)
print()

# Result: increasing density with increasing salinity
# ======================================================================================================================


""" Orbital Mechanics for Engineering Students Problem 1.23
Question:
Using an RK solver, solve the nonlinear Lorenz Equations:
    https://journals.ametsoc.org/jas/article/20/2/130/16956/Deterministic-Nonperiodic-Flow
    xDot = sigma(y-x)
    yDot = x(rho-z)-y
    zDot = xy - beta z
with:
    - sigma = 10
    - beta = 8/3
    - rho = 28
    - x = 0, y = 1, z = 0 at t = 0
Plot the trajectory x, y, z in three dimensions to see the Lorenz attractor.
Written by: J.X.J. Bannwarth
"""
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from orbitutils.solvers import rkf45

# Differential equations
def rates(t, Y, sigma=10., beta=8./3., rho=28.):
    F = np.zeros(Y.shape)
    F[0] = sigma*(Y[1]-Y[0])
    F[1] = Y[0]*(rho-Y[2]) - Y[1]
    F[2] = Y[0]*Y[1] - beta*Y[2]
    return F

# Title
print("Orbital Mechanics for Engineering Students Problem 1.23")

# Parameters
tSpan = np.array([0., 20.])
Y0 = np.array([0., 1., 0.])

# Solve numerically
y, t = rkf45(rates, Y0, tSpan)

# Plot answer
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.plot(y[:,0],y[:,1],y[:,2], label='$Y(t)$')
ax.scatter(Y0[0],Y0[1],Y0[2], color="red", label='$Y_0$')
ax.set_xlabel("$x$")
ax.set_ylabel("$y$")
ax.set_zlabel("$z$")
ax.set_title("Lorenz attractor")
ax.legend()
plt.show()


"""Module with activation function definitions."""

import numpy as np

def _identity(x: np.ndarray, derivative: bool = False) -> np.ndarray:
    """Identity activation function.

    Args:
        x: Input for function.
        derivative: Flag for calculating function derivative.

    Returns:
        Result.

    """
    if derivative:
        return np.ones_like(x)
    return x

def _relu(x: np.ndarray, derivative: bool = False) -> np.ndarray:
    """ReLu activation function.

    Args:
        x: Input for function.
        derivative: Flag for calculating function derivative.

    Returns:
        Result.

    """
    # pylint: disable=no-member
    if derivative:
        return np.heaviside(x, 0)
    return np.maximum(x, 0)

def _sigmoid(x: np.ndarray, derivative: bool = False) -> np.ndarray:
    """Sigmoid activation function.

    Args:
        x: Input for function.
        derivative: Flag for calculating function derivative.

    Returns:
        Result.

    """
    x = np.maximum(x, -100.0)
    sigmoid = 1 / (1 + np.exp(-x))
    if derivative:
        return sigmoid * (1 - sigmoid)
    return sigmoid

def _softplus(x: np.ndarray, derivative: bool = False) -> np.ndarray:
    """Softplus activation function.

    Args:
        x: Input for function.
        derivative: Flag for calculating function derivative.

    Returns:
        Result.

    """
    x = np.maximum(x, -100.0)
    if derivative:
        return 1 / (1 + np.exp(-x))
    return x + np.log(1 + np.exp(-x))

def _tanh(x: np.ndarray, derivative: bool = False) -> np.ndarray:
    """Hyperbolic tangent activation function.

    Args:
        x: Input for function.
        derivative: Flag for calculating function derivative.

    Returns:
        Result.

    """
    tanh = np.tanh(x)
    if derivative:
        return 1 - tanh * tanh
    return tanh

_ACTIVATION_DICT = {
    'identity': _identity,
    'relu': _relu,
    'sigmoid': _sigmoid,
    'softplus': _softplus,
    'tanh': _tanh}


# -*- coding: utf-8 -*-
"""
Sistemi lineari
"""
import numpy as np
def solveL(L,b):
    # L matrice triangolare inferiore
    m,n = L.shape
    if m != n:
        print("matrice non quadrata")
        return [],1
    # Test singolarita, se L e triangolare non singolare
    # Nella diagonale tutti gli elementi sono non nulli
    # --------------------------------------------------
    # np all verifica che tutti gli elementi lungo un asse siano "True",
    # diag restituisce un array [...] like
    if np.all(np.diag(L)) != True:
        print("elemento diagonale nullo")
        return[],1
    x = np.zeros((n,1))
    for i in range(n):
        sommatoria = np.dot(L[i,:i],x[:i])
        x[i] = (b[i] - sommatoria)/L[i,i]
    return x,0

def solveU(U,b):
    # U matrice triangolare superiore
    m,n = U.shape
    if m != n:
        print("matrice non quadrata")
        return [],1
    if np.all(np.diag(U)) != True:
        print("elemento diagonale nullo")
        return[],1
    x = np.zeros((n,1))
    for i in reversed(range(n)):
        sommatoria = np.dot(U[i,i+1:n], x[i+1:n])
        x[i] = (b[i] - sommatoria)/U[i:i]
    return x,0

# scambia la k-esima riga con la p-esima
def SwapRows(M, k, p):
    M[[k,p],:] = M[[p,k],:]

def fattorizzazioneLU(A, pivoting = 0):
    m,n = A.shape
    if m != n:
        print("Matrice non quadrata:")
        return [],[],[],1 # P,L,U,flag
    P = np.eye(n)
    U = A.copy()
    for k in range(n-1):
        # Test su elemento pivotale
        if U[k,k] == 0:
            print("Elemento pivotale nullo")
            return [],[],[],1
        p = np.argmax(abs(U[k,k])) + k
        if pivoting and p != k:
            SwapRows(U,k,p)
            SwapRows(P,k,p)
        # Eliminazione gaussiana
        U[k+1:n,k] = U[k+1:n,k]/U[k,k] # Memorizzo i moltiplicatori della k-esima colonna
        U[k+1:n,k+1:n] = U[k+1:n,k+1:n] - np.outer(U[k+1:n,k],U[k,k+1:n]) # El. gaussiana sulla matrice
    L = np.tril(U,-1) + np.eye(n)
    U = np.triu(U) # estrae la triangolare superiore, con diagonale
    return P,L,U,0

def solveLU(P,L,U,b):
    y,flag = solveL(L,np.dot(P,b))
    if flag:
        return [],1
    return solveU(U,y)


"""
File: interpolate_exp_cos.py
Copyright (c) 2016 Andrew Malfavon
License: MIT
Exercise B.2
Description: Interpolate a discrete function.
"""

import numpy as np
import matplotlib.pyplot as plt

#plots the given function for eps>0 and n nodes.
def plot(eps, n):
    x = np.linspace(0, 1, n + 1)
    y = np.sin(1 / (x + (eps)))
    plt.plot(x, y)
    plt.axis([-0.2, 1.1, -1.1, 1.1])
    plt.xlabel('x')
    plt.ylabel('y')
    plt.title('f(x)')

#plots for n and n+10
def plot_refined(eps, n1):
    n2 = n1 + 10
    x1 = np.linspace(0, 1, n1 + 1)
    y1 = np.sin(1 / (x1 + (eps)))
    x2 = np.linspace(0, 1, n2 + 1)
    y2 = np.sin(1 / (x2 + (eps)))
    plt.plot(x1, y1)
    plt.plot(x2, y2)
    plt.axis([-0.2, 1.1, -1.1, 1.1])
    plt.xlabel('x')
    plt.ylabel('y')
    plt.title('f(x)')

#Find how large n should be for the difference between the two functions to be less than 0.1
def find_large_n(eps):
    n1 = 1
    n2 = n1 + 10
    x1 = np.linspace(0, 1, n1 +1)
    x2 = np.linspace(0, 1, n2 + 1)
    f1 = np.sin(1 / (x1 + (eps)))
    f2 = np.sin(1 / (x2 + (eps)))
    while (abs(max(f2) - max(f1)) >= 0.1):
        n1 += 1
        n2 += 1
        x1 = np.linspace(0, 1, n1 +1)
        x2 = np.linspace(0, 1, n2 + 1)
        f1 = np.sin(1 / (x1 + (eps)))
        f2 = np.sin(1 / (x2 + (eps)))
    return n1

#test that for each eps the difference of the max values of the functions is less than 0.1
def test():
    eps_array = [0.1, 0.2, 0.05]#each eps value to be tested
    difference_array = []
    for elem in eps_array:#use the n1 calculated in find_large_n
        n1 = find_large_n(elem)
        n2 = n1 + 10
        x1 = np.linspace(0, 1, n1 +1)
        x2 = np.linspace(0, 1, n2 + 1)
        f1 = np.sin(1 / (x1 + (elem)))
        f2 = np.sin(1 / (x2 + (elem)))
        difference_array.append(abs(max(f1) - max(f2)))#make an array of each difference
    for i in range(len(difference_array)):
        assert difference_array[i] < 0.1#check the resulting difference is less than 0.1


# -*- encoding: utf-8 -*-

import numpy as np

class PCA(object):
    """Dimension Reduction using Principal Component Analysis (PCA)

    It is the procces of computing principal components which explains the
    maximum variation of the dataset using fewer components.

    :type  n_components: int, optional
    :param n_components: Number of components to consider, if not set then
                         `n_components = min(n_samples, n_features)`, where
                         `n_samples` is the number of samples, and
                         `n_features` is the number of features (i.e.,
                         dimension of the dataset).

    :Attributes:
    :type  covariance_: np.ndarray
    :param covariance_: Coviarance Matrix

    :type  eig_vals_: np.ndarray
    :param eig_vals_: Calculated Eigen Values

    :type  eig_vecs_: np.ndarray
    :param eig_vecs_: Calculated Eigen Vectors

    :type  explained_variance_: np.ndarray
    :param explained_variance_: Explained Variance of Each Principal Components

    :type  cum_explained_variance_: np.ndarray
    :param cum_explained_variance_: Cumulative Explained Variables
    """

    def __init__(self, n_components : int = None):
        """Default Constructor for Initialization"""

        self.n_components = n_components

    def fit_transform(self, X : np.ndarray):
        """Fit the PCA algorithm into the Dataset"""

        if not self.n_components:
            self.n_components = min(X.shape)

        self.covariance_ = np.cov(X.T)

        # calculate eigens
        self.eig_vals_, self.eig_vecs_ = np.linalg.eig(self.covariance_)
        # self.eig_pairs_ = [(np.abs(self.eig_vals_[i]), self.eig_vecs_[:, i]) for i in range(len(self.eig_vals_))]

        # explained variance
        _tot_eig_vals = sum(self.eig_vals_)
        self.explained_variance_ = np.array([(i / _tot_eig_vals) * 100 for i in sorted(self.eig_vals_, reverse = True)])
        self.cum_explained_variance_ = np.cumsum(self.explained_variance_)

        # define `W` as `d x k`-dimension
        self.W_ = self.eig_vecs_[:, :self.n_components]

        print(X.shape, self.W_.shape)
        return X.dot(self.W_)


#Write the representation of ℜ2 as a list comprehension - use ranges between -10 and 10 for all values of x and y.
#Write the representation of ℜ3 as a list comprehension - use ranges between -10 and 10 for all values of x, y, and z.
#Write a list comprehension that represents the the set V = {(x, y, z) | x, y, z ∈ ℜ and x+y = 11}. Use ranges between -10 and 10 for all values of x, y, and z.
#Choose three values of x, y, and z that show the set V = {(x, y, z) | x, y, z ∈ ℜ and x+y = 11} is not a subspace of ℜ3. These values should represent a tuple that would be in vector V had it been a vector subspace. Each value should also be between -10 and 10.

import numpy as np
R2 = [[x, y] for x in range(-10, 10) for y in range(-10, 10)]
R3 = [[x,y,z] for x in range(-10, 10) for y in range(-10,10) for z in range(-10, 10)]
V = [[x,y,z] for x in range(-10, 10) for y in range(-10,10) for z in range(-10, 10) if x + y == 11]
not_sub = [[x,y,z] for x in range(-10, 10) for y in range(-10,10) for z in range(-10, 10) if x + y != 11]


"""
Implementing second moments of area from

https://en.wikipedia.org/wiki/List_of_second_moments_of_area

"""
import numpy as np

class SecondMoment():

    def __init__(self):
        return

    @staticmethod
    def RectArea(b, h):
        """
                /\ y
                |
             b  |
            ---------
            |       |
            |       |
            |       |  h --->x
            |       |
            ---------
        :param b: width
        :param h: heigth
        :return: The second moments of area [Ixx, Iyy]
        """

        return [ (b*h**3 )/12, (b**3*h)/12 ]

    @staticmethod
    def PolarRectArea(b, h):
        """
             /\ y
                |
             b  |
            ---------
            |       |
            |       |
            |       |  h --->x
            |       |
            ---------

        :param b:
        :param h:
        :return:

        """
        # from  https://www.engineersedge.com/polar-moment-inertia.htm
        return (b * h*(b**2 + h**2))/12

    @staticmethod
    def CircularArea(r):
        """

        :param r: radius
        :return:
        """

        return [(np.pi * r**4)/4 , (np.pi * r**4) / 4]

    @staticmethod
    def PolarCircularArea(r):
        """
        https://www.fxsolver.com/browse/formulas/Polar+Moment+of+Area+-+Circular+Cross+Section
        :param r:  radius
        :return:
        """
        return (np.pi * r ** 4) / 2


import numpy as np
import matplotlib.pyplot as plt
import time
import tqdm
import copy

from pre_process import load_mat, remove_mean, separate_data, compute_eigenvalues_eigenvectors

def sort_eigenvalues_eigenvectors(eigenvalues, eigenvectors):
    p = np.argsort(np.abs(eigenvalues))[::-1]
    eigenvalues_ = copy.deepcopy(eigenvalues[p])
    eigenvectors_ = copy.deepcopy(np.real(eigenvectors[:,p]))
    return eigenvalues_, eigenvectors_

def non_zero(eigenvalues,lim=0.001):
    count = 0
    print(eigenvalues.shape)
    for i in eigenvalues:
        if i<lim:
            count+=1
    return count

def compare(a1, a2, lim=0.001):
    N = a1.shape[0] if a1.shape[0] < a2.shape[0] else a2.shape[0]
    for i in range(N):
        if np.abs(a1[i]-a2[i])>lim:
            return False
    return True

def main():
    # Load the data from the matrix
    X, Y = load_mat("data/face.mat")
    dataset = separate_data((X,Y))
    X_train = dataset[0][0]

    # Compute and remove mean from the training dataset
    mean = X_train.mean(axis=1).reshape(-1,1)

    A = X_train - mean

    #Display the mean face for verification
    plt.figure()
    plt.title('Mean Face')
    mean = mean.reshape((46,56))
    mean = np.rot90(mean,3)
    plt.imshow(mean, cmap="gray")
    plt.savefig("results/q1/mean.png", format="png", transparent=True) # TODO: add eigenface also

    D, N = X_train.shape
    start = time.time()
    S_naive = copy.deepcopy((1 / N) * np.dot(A, A.T))
    end = time.time()
    print("Calculated S inefficiently, took: {} s".format(end-start))

    start = time.time()
    S_efficient = copy.deepcopy((1/N) * np.dot(A.T, A))
    end = time.time()
    print("Calculated S efficiently, took: {} s".format(end-start))

    start = time.time()
    _l_naive,_u_naive = np.linalg.eig(S_naive)
    end = time.time()
    print("Calculated eigenvectors, eigenvalues efficiently, took: {} s".format(end-start))

    start = time.time()
    _l_efficient,_u_efficient = np.linalg.eig(S_efficient)
    end = time.time()
    print("Calculated eigenvectors, eigenvalues inefficiently, took: {} s".format(end-start))

    _u_efficient = copy.deepcopy(np.matmul(A,_u_efficient))
    _u_efficient = _u_efficient / np.linalg.norm(_u_efficient,axis=0)
    indexes = np.argsort(np.abs(_l_naive))[::-1]
    u_naive = np.real(_u_naive[:, indexes])
    l_naive = _l_naive[indexes]

    _indexes = np.argsort(np.abs(_l_efficient))[::-1]
    u_efficient = np.real(_u_efficient[:, _indexes])
    l_efficient = _l_efficient[_indexes]

    print("Non-zero eigenvalues inefficiently {}".format(non_zero(l_naive)))
    print("Non-zero eigenvalues efficiently {}".format(non_zero(l_efficient)))

    print("Are eigenvalues for both the same? {}".format(compare(l_efficient, l_naive)))
    for i in range(u_efficient.shape[1]):
        print(u_efficient[:,i], u_naive[:,i])
        print("Are eigenvectors for both the same? {}".format(compare(u_efficient[:,i], u_naive[:,i])))

    l_naive_sum = np.sum(l_naive)
    k = [0] * N

    for i in range(N):
        for j in range(i):
            k[i]+=l_naive[j]
        k[i]/=l_naive_sum
    k = np.array(k)
    print("Relative accurracy for M=10: {}, M=50: {}, M = 100: {}, M=200: {}".format(k[10], k[50], k[100], k[200]))

    plt.figure()
    plt.title('Relative Reconstruction Error')
    plt.xlabel('$\lambda_{i}$ eigenvalue sum')
    plt.ylabel('Accuracy')
    plt.plot(k)
    plt.savefig('results/q1/relative_accuracy.png',
                format='png', transparent=True)

    plt.figure()
    plt.title('Efficient Eigenvalues')
    plt.xlabel('$l_{m}$ eigenvalue')
    plt.ylabel('Real Value')
    plt.plot(l_efficient)
    plt.savefig('results/q1/eigenvalues_efficient.png',
                format='png', transparent=True)

    plt.figure()
    plt.title('Naive Eigenvalues')
    plt.xlabel('$l_{m}$ eigenvalue')
    plt.ylabel('Real Value')
    plt.plot(l_naive)
    plt.savefig('results/q1/eigenvalues_naive.png',
                format='png', transparent=True)

    plt.figure()
    plt.title('Combined Efficient and Naive Eigenvalues')
    plt.xlabel('$l_{m}$ eigenvalue')
    plt.ylabel('Real Value')
    plt.plot(l_naive, label="Naive implementation")
    plt.plot(l_efficient, label="Efficient implementation")
    plt.legend()
    plt.savefig('results/q1/eigenvalues_efficient_naive.png',
                format='png', transparent=True)

    f, axarr = plt.subplots(2,3)
    for i in range(3):
        img = np.real(u_naive[:,i].reshape((46,56)))
        img = np.rot90(img,3)
        axarr[0,i].imshow(img, cmap=plt.cm.Greys)
        axarr[0,i].set_title("Eigenface %d" %i)
    for i in range(3):
        img = np.real(u_efficient[:,i].reshape((46,56)))
        img = np.rot90(img,3)
        axarr[1,i].imshow(img, cmap=plt.cm.Greys)

    plt.savefig("results/q1/eigenfaces.png", format="png", transparent=True)

    # Plot mean and eigenface together

    plt.close()

if __name__ == '__main__':
    main()


import numpy as np
import matplotlib.pyplot as plt

def read_csv_file(file):
    # Read data from CSV file
    data = np.genfromtxt(file, delimiter=',', dtype=np.float, skip_header=True)
    return data

def mse(y_prediction, y_true):
    # Calculate mean square error (mse)
    return np.average((y_true - y_prediction) ** 2)

def rmse(y_prediction, y_true):
    # Calculate root mean square error (mse)
    return np.sqrt(mse(y_prediction, y_true))

def train_test_split(x, y, train_size=0.8):
    # Split data into training and test set
    train_size = int(len(x) * train_size)

    x_train = x[:train_size]
    y_train = y[:train_size]

    x_test = x[train_size:]
    y_test = y[train_size:]

    return x_train, x_test, y_train, y_test

def normal_equation(x, y):
    # Linear regression via normal equation
    return np.matmul(np.matmul(np.linalg.inv(np.matmul(x.T, x)), x.T), y)

def gradient_descent(x, y, learning_rate=0.001, nr_of_epochs=10000):
    # Linear regression via gradient descent
    weights = np.zeros(x.shape[1])
    error_mse = []
    error_rmse = []

    for i in range(nr_of_epochs):

        # Calculate output (= prediction) of activation function
        prediction = predict(x, weights)

        # Calculate error between prediction and output variable
        errors = (y - prediction)

        # Calculate gradient descent to update weights
        gradient = np.dot(x.T, errors)
        weights += learning_rate * gradient

        error_mse.append(mse(prediction, y))
        error_rmse.append(rmse(prediction, y))

    return weights, error_mse, error_rmse

def predict(x, weights):
    # Predict output (i.e. MEDV) based on given features (x) and weights
    return np.dot(x, weights)

def print_subplot(axs, error_rmse, rmse_normal_equation, learning_rate):
    axs.plot(error_rmse)
    axs.set_title('Learning rate {:0.5f}'.format(learning_rate))
    axs.set_xlabel('Nr. of Epochs')
    axs.set_ylabel('RMSE')
    axs.axhline(rmse_normal_equation, color='red', linestyle='dotted')
    axs.set_ylim([0, 30])

def main():

    boston_housing_data = read_csv_file('mass_boston.csv')

    # Change printing precision of matrices
    np.set_printoptions(precision=3)

    # Remove entries where MEDV is equal to 50.0, as this variable has been censored for higher values
    boston_housing_data = boston_housing_data[boston_housing_data[:, 13] != 50.0]

    # Split features and output variable (y = median house value)
    features = boston_housing_data[:, 0:13]
    y = boston_housing_data[:, 13]

    # Normalise features by min-max
    min = features.min(axis=0)
    max = features.max(axis=0)
    norm_features = (features - min) / (max - min)

    # Covariance matrix to find features with highest covariance with output variable
    cov = np.corrcoef(boston_housing_data.T)

    plt.matshow(cov, cmap='seismic')
    plt.xticks(np.arange(14))
    plt.yticks(np.arange(14))
    plt.show()

    print("Correlation with MEDV {}\n".format(cov[13]))

    # Select features with highest covariance (2 = INDUS, 4 = NOX, 5 = RM, 9 = TAX, 10 = PTRATIO, 12 = LSTAT)
    selected_features = norm_features[:, [2, 4, 5, 9, 10, 12]]

    # Add bias
    x = np.c_[np.ones((len(selected_features))), selected_features]

    # Split data into train and test
    x_train, x_test, y_train, y_test = train_test_split(x, y, train_size=0.8)

    print("Linear regression via normal equation")

    weights = normal_equation(x_train, y_train)
    predictions = predict(x_train, weights)

    print("Weights {}".format(weights))
    rmse_normal_equation = rmse(predictions, y_train)

    print("Train: MSE {:2.4f} / RMSE {:1.4f}".format(mse(predictions, y_train), rmse(predictions, y_train)))

    predictions = predict(x_test, weights)

    print("Test: MSE {:2.4f} / RMSE {:1.4f}\n".format(mse(predictions, y_test), rmse(predictions, y_test)))

    fig, axs = plt.subplots(2)
    plt.subplots_adjust(hspace=0.6)

    # Linear regression via gradient descent with optimal learning rate
    nr_of_epochs = 1000
    learning_rate = 0.001

    print("Linear regression via gradient descent (learning rate {:0.5f})".format(learning_rate))

    weights, error_mse, error_rmse = gradient_descent(x_train, y_train, learning_rate, nr_of_epochs)
    predictions = predict(x_train, weights)

    print("Weights {}".format(weights))

    print("Train: MSE {:2.4f} / RMSE {:1.4f}".format(mse(predictions, y_train), rmse(predictions, y_train)))

    predictions = predict(x_test, weights)

    print("Test: MSE {:2.4f} / RMSE {:1.4f}\n".format(mse(predictions, y_test), rmse(predictions, y_test)))

    print_subplot(axs[0], error_rmse, rmse_normal_equation, learning_rate)

    # Linear regression via gradient descent with small learning rate
    learning_rate = 0.00001

    print("Linear regression via gradient descent (learning rate {:0.5f})".format(learning_rate))

    weights, error_mse, error_rmse = gradient_descent(x_train, y_train, learning_rate, nr_of_epochs)
    predictions = predict(x_train, weights)

    print("Weights {}".format(weights))

    print("Train: MSE {:2.4f} / RMSE {:1.4f}".format(mse(predictions, y_train), rmse(predictions, y_train)))

    predictions = predict(x_test, weights)

    print("Test: MSE {:2.4f} / RMSE {:1.4f}\n".format(mse(predictions, y_test), rmse(predictions, y_test)))

    print_subplot(axs[1], error_rmse, rmse_normal_equation, learning_rate)

    plt.show()

if __name__ == '__main__':
    main()


# 1.Write a NumPy program to get the numpy version and show numpy build configuration.
import numpy as np

print(np.__version__)
print(np.show_config())
# 2. Write a NumPy program to  get help on the add function.
import numpy as np

print(np.info(np.add))
# 3. Write a NumPy program to test whether none of the elements of a given array is zero.
import numpy as np

x = np.array([1, 2, 3, 4])
print("Original array:")
print(x)
print("Test if none of the elements of the said array is zero:")
print(np.all(x))
x = np.array([0, 1, 2, 3])
print("Original array:")
print(x)
print("Test if none of the elements of the said array is zero:")
print(np.all(x))
# 4. Write a NumPy program to test whether any of the elements of a given array is non-zero.
import numpy as np

x = np.array([1, 0, 0, 0])
print("Original array:")
print(x)
print("Test whether any of the elements of a given array is non-zero:")
print(np.any(x))
x = np.array([0, 0, 0, 0])
print("Original array:")
print(x)
print("Test whether any of the elements of a given array is non-zero:")
print(np.any(x))
# 5. Write a NumPy program to test a given array element-wise for finiteness (not infinity or not a Number).
import numpy as np

a = np.array([1, 0, np.nan, np.inf])
print("Original array")
print(a)
print("Test a given array element-wise for finiteness :")
print(np.isfinite(a))
# 6. Write a NumPy program to test element-wise for positive or negative infinity.
import numpy as np

a = np.array([1, 0, np.nan, np.inf])
print("Original array")
print(a)
print("Test element-wise for positive or negative infinity:")
print(np.isinf(a))
# 7. Write a NumPy program to test element-wise for NaN of a given array.
import numpy as np

a = np.array([1, 0, np.nan, np.inf])
print("Original array")
print(a)
print("Test element-wise for NaN:")
print(np.isnan(a))
# 8. Write a NumPy program to test element-wise for complex number, real number of a given array. Also test whether a given number is a scalar type or not.
import numpy as np

a = np.array([1 + 1j, 1 + 0j, 4.5, 3, 2, 2j])
print("Original array")
print(a)
print("Checking for complex number:")
print(np.iscomplex(a))
print("Checking for real number:")
print(np.isreal(a))
print("Checking for scalar type:")
print(np.isscalar(3.1))
print(np.isscalar([3.1]))
# 9. Write a NumPy program to test whether two arrays are element-wise equal within a tolerance.
import numpy as np

print("Test if two arrays are element-wise equal within a tolerance:")
print(np.allclose([1e10, 1e-7], [1.00001e10, 1e-8]))
print(np.allclose([1e10, 1e-8], [1.00001e10, 1e-9]))
print(np.allclose([1e10, 1e-8], [1.0001e10, 1e-9]))
print(np.allclose([1.0, np.nan], [1.0, np.nan]))
print(np.allclose([1.0, np.nan], [1.0, np.nan], equal_nan=True))
# 10. Write a NumPy program to create an element-wise comparison (greater, greater_equal, less and less_equal) of two given arrays
import numpy as np

x = np.array([3, 5])
y = np.array([2, 5])
print("Original numbers:")
print(x)
print(y)
print("Comparison - greater")
print(np.greater(x, y))
print("Comparison - greater_equal")
print(np.greater_equal(x, y))
print("Comparison - less")
print(np.less(x, y))
print("Comparison - less_equal")
print(np.less_equal(x, y))
# 11. Write a NumPy program to create an element-wise comparison (equal, equal within a tolerance) of two given arrays
import numpy as np

x = np.array([72, 79, 85, 90, 150, -135, 120, -10, 60, 100])
y = np.array([72, 79, 85, 90, 150, -135, 120, -10, 60, 100.000001])
print("Original numbers:")
print(x)
print(y)
print("Comparison - equal:")
print(np.equal(x, y))
print("Comparison - equal within a tolerance:")
print(np.allclose(x, y))
# 12. Write a NumPy program to create an array with the values 1, 7, 13, 105 and determine the size of the memory occupied by the array
import numpy as np

X = np.array([1, 7, 13, 105])
print("Original array:")
print(X)
print("Size of the memory occupied by the said array:")
print("%d bytes" % (X.size * X.itemsize))
# 13. Write a NumPy program to create an array of 10 zeros,10 ones, 10 fives.
import numpy as np

array = np.zeros(10)
print("An array of 10 zeros:")
print(array)
array = np.ones(10)
print("An array of 10 ones:")
print(array)
array = np.ones(10) * 5
print("An array of 10 fives:")
print(array)
# 14. Write a NumPy program to create an array of the integers from 30 to70.
import numpy as np

array = np.arange(30, 71)
print("Array of the integers from 30 to70")
print(array)
# 15. Write a NumPy program to create an array of all the even integers from 30 to 70.
import numpy as np

array = np.arange(30, 71, 2)
print("Array of all the even integers from 30 to 70")
print(array)
# 16. Write a NumPy program to create a 3x3 identity matrix.
import numpy as np

array_2D = np.identity(3)
print('3x3 matrix:')
print(array_2D)
# 17. Write a NumPy program to generate a random number between 0 and 1.
import numpy as np

rand_num = np.random.normal(0, 1, 1)
print("Random number between 0 and 1:")
print(rand_num)
# 18. Write a NumPy program to generate an array of 15 random numbers from a standard normal distribution.
import numpy as np

rand_num = np.random.normal(0, 1, 15)
print("15 random numbers from a standard normal distribution:")
print(rand_num)
# 19. Write a NumPy program to create a vector with values ranging from 15 to 55 and print all values except the first and last.
import numpy as np

v = np.arange(15, 55)
print("Original vector:")
print(v)
print("All values except the first and last of the said vector:")
print(v[1:-1])
# 20. Write a NumPy program to create a 3X4 array using and iterate over it.
import numpy as np

a = np.arange(10, 22).reshape((3, 4))
print("Original array:")
print(a)
print("Each element of the array is:")
for x in np.nditer(a):
    print(x, end=" ")
# 20. Write a NumPy program to create a 3X4 array using and iterate over it.
import numpy as np

a = np.arange(10, 22).reshape((3, 4))
print("Original array:")
print(a)
print("Each element of the array is:")
for x in np.nditer(a):
    print(x, end=" ")


# https://www.hackerrank.com/challenges/np-array-mathematics/problem

# Inputs
standard_input = """2 4
1 2 3 4
1 2 3 4
5 6 7 7
5 6 7 7"""

# no [use-before-def]
m = None

import numpy

m, n = [int(s) for s in input().split()]
# 2 4

a = numpy.array([input().split() for _ in range(m)], int)
# 1 2 3 4
# 1 2 3 4

b = numpy.array([input().split() for _ in range(m)], int)
# 5 6 7 7
# 5 6 7 7

print(a + b)
print(a - b)
print(a * b)
print(a // b)
print(a % b)
print(a ** b)
# [[ 6  8 10 11]
#  [ 6  8 10 11]]
# [[-4 -4 -4 -3]
#  [-4 -4 -4 -3]]
# [[ 5 12 21 28]
#  [ 5 12 21 28]]
# [[0 0 0 0]
#  [0 0 0 0]]
# [[1 2 3 4]
#  [1 2 3 4]]
# [[    1    64  2187 16384]
#  [    1    64  2187 16384]]

""" Reference

a = numpy.array([1, 2, 3, 4], float)
b = numpy.array([5, 6, 7, 8], float)

print(a + b)
print(numpy.add(a, b))
# [ 6.  8. 10. 12.]
# [ 6.  8. 10. 12.]

print(a - b)
print(numpy.subtract(a, b))
# [-4. -4. -4. -4.]
# [-4. -4. -4. -4.]

print(a * b)
print(numpy.multiply(a, b))
# [ 5. 12. 21. 32.]
# [ 5. 12. 21. 32.]

print(a / b)
print(numpy.divide(a, b))
# [0.2        0.33333333 0.42857143 0.5       ]
# [0.2        0.33333333 0.42857143 0.5       ]

print(a % b)
print(numpy.mod(a, b))
# [1. 2. 3. 4.]
# [1. 2. 3. 4.]

print(a ** b)
print(numpy.power(a, b))
# [1.0000e+00 6.4000e+01 2.1870e+03 6.5536e+04]
# [1.0000e+00 6.4000e+01 2.1870e+03 6.5536e+04]

"""

__Training__/Python - HackerRank/16. Numpy/Array Mathematics.py


import numpy as np
import scipy
import scipy.spatial.distance as sd

from UQpy.utilities.ValidationTypes import RandomStateType, Numpy2DFloatArray
from UQpy.utilities.kernels import EuclideanKernel
from scipy.spatial.distance import pdist

class GaussianKernel(EuclideanKernel):
    """
    A class to calculate the Gaussian kernel defined as:

    .. math::
        k(x_j, x_i) = \exp[-(x_j - xj)^2/4\epsilon]

    """
    def __init__(self, epsilon: float = 1.0):
        """
        :param epsilon: Scale parameter of the Gaussian kernel
        """
        super().__init__()
        self.epsilon = epsilon

    def kernel_entry(self, xi: Numpy2DFloatArray, xj: Numpy2DFloatArray):
        """
        Given two points, this method computes the Gaussian kernel value between those two points

        :param xi: First point.
        :param xj: Second point.
        :return: Float representing the kernel entry.
        """
        if len(xi.shape) == 1:
            d = pdist(np.array([xi, xj]), "sqeuclidean")
        else:
            d = np.linalg.norm(xi-xj, 'fro') ** 2
        return np.exp(-d / (2*self.epsilon**2))

    def optimize_parameters(self, data: np.ndarray, tolerance: float,
                            n_nearest_neighbors: int,
                            n_cutoff_samples: int,
                            random_state: RandomStateType = None):
        """

        :param data: Set of data points.
        :param tolerance: Tolerance below which the Gaussian kernel is assumed to be zero.
        :param n_nearest_neighbors: Number of neighbors to use for cut-off estimation.
        :param n_cutoff_samples: Number of samples to use for cut-off estimation.
        :param random_state: Random seed used to initialize the pseudo-random number generator. If an :any:`int` is
         provided, this sets the seed for an object of :class:`numpy.random.RandomState`. Otherwise, the
         object itself can be passed directly.
        """

        cut_off = self._estimate_cut_off(data, n_nearest_neighbors, n_cutoff_samples, random_state)
        self.epsilon = cut_off ** 2 / (-np.log(tolerance))

    def _estimate_cut_off(self, data, n_nearest_neighbors, n_partition, random_state):
        data = np.atleast_2d(data)
        n_points = data.shape[0]
        if n_points < 10:
            d = scipy.spatial.distance.pdist(data)
            return np.max(d)

        if n_partition is not None:
            random_indices = np.random.default_rng(random_state).permutation(n_points)
            distance_matrix = sd.cdist(data[random_indices[:n_partition]], data, metric='euclidean')
        else:
            distance_matrix = sd.squareform(sd.pdist(data, metric='euclidean'))
        k = np.min([n_nearest_neighbors, distance_matrix.shape[1]])
        k_smallest_values = np.partition(distance_matrix, k - 1, axis=1)[:, k - 1]

        est_cutoff = np.max(k_smallest_values)
        return float(est_cutoff)


# Question 1 Lab Assignment 02
# @AB Satyaprakash, 180123062

# imports
from math import exp, sqrt
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# functions ---------------------------------------------------------------------------

def getOptionPrice(S0, K, T, r, sig, M, p, u, d, dt):
    callList = [0]*(M+1)
    putList = [0]*(M+1)

    for i in range(M+1):
        callList[i] = max(S0*(u**i)*(d**(M-i)) - K, 0)
        putList[i] = max(0, K - S0*(u**i)*(d**(M-i)))

    for i in range(M):
        for j in range(M-i):
            callList[j] = ((1-p)*callList[j] + p*callList[j+1])*exp(-r*dt)
            putList[j] = ((1-p)*putList[j] + p*putList[j+1])*exp(-r*dt)
    return callList[0], putList[0]

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

# Find the Initial prices of European Put and Call Options
# Given Initial Values
S0, K, T, M, r, sig = 100, 100, 1, 100, 0.08, 0.2

dt = T/M

u1 = exp(sig*sqrt(dt))
d1 = exp(-sig*sqrt(dt))
p1 = (exp(r*dt)-d1)/(u1-d1)

u2 = exp(sig*sqrt(dt)+(r-sig*sig/2)*dt)
d2 = exp(-sig*sqrt(dt)+(r-sig*sig/2)*dt)
p2 = (exp(r*dt)-d2)/(u2-d2)

callp1, putp1 = getOptionPrice(S0, K, T, r, sig, M, p1, u1, d1, dt)
callp2, putp2 = getOptionPrice(S0, K, T, r, sig, M, p2, u2, d2, dt)

print('The Call price for set 1 is: {}'.format(callp1))
print('The Put price for set 1 is: {}'.format(putp1))
print('The Call price for set 2 is: {}'.format(callp2))
print('The Put price for set 2 is: {}'.format(putp2))

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

# Draw 2-D Plots
# (a) S0 from 50 - 150 in steps of 5
S0 = np.arange(50, 155, 5)
callp1, putp1, callp2, putp2 = np.zeros(S0.shape[0]), np.zeros(
    S0.shape[0]), np.zeros(S0.shape[0]), np.zeros(S0.shape[0])

for i in range(S0.shape[0]):
    K, T, M, r, sig = 100, 1, 100, 0.08, 0.2
    dt = T/M

    u1 = exp(sig*sqrt(dt))
    d1 = exp(-sig*sqrt(dt))
    p1 = (exp(r*dt)-d1)/(u1-d1)

    u2 = exp(sig*sqrt(dt)+(r-sig*sig/2)*dt)
    d2 = exp(-sig*sqrt(dt)+(r-sig*sig/2)*dt)
    p2 = (exp(r*dt)-d2)/(u2-d2)

    callp1[i], putp1[i] = getOptionPrice(S0[i], K, T, r, sig, M, p1, u1, d1, dt)
    callp2[i], putp2[i] = getOptionPrice(S0[i], K, T, r, sig, M, p2, u2, d2, dt)

plt.plot(S0, callp1, color='red')
plt.title('Plot of Call option at t=0 vs S(0) (50-150) for Set 1 ')
plt.xlabel('Stock Price at t = 0')
plt.ylabel('Price of Call option')
plt.show()

plt.plot(S0, putp1, color='green')
plt.title('Plot of Put option at t=0 vs S(0) (50-150) for Set 1 ')
plt.xlabel('Stock Price at t = 0')
plt.ylabel('Price of Put option')
plt.show()

plt.plot(S0, callp2, color='red')
plt.title('Plot of Call option at t=0 vs S(0) (50-150) for Set 2 ')
plt.xlabel('Stock Price at t = 0')
plt.ylabel('Price of Call option')
plt.show()

plt.plot(S0, putp2, color='green')
plt.title('Plot of Put option at t=0 vs S(0) (50-150) for Set 2 ')
plt.xlabel('Stock Price at t = 0')
plt.ylabel('Price of Put option')
plt.show()

# (b) K from 50 - 150 in steps of 5
K = np.arange(50, 155, 5)
callp1, putp1, callp2, putp2 = np.zeros(K.shape[0]), np.zeros(
    K.shape[0]), np.zeros(K.shape[0]), np.zeros(K.shape[0])

for i in range(K.shape[0]):
    S0, T, M, r, sig = 100, 1, 100, 0.08, 0.2
    dt = T/M

    u1 = exp(sig*sqrt(dt))
    d1 = exp(-sig*sqrt(dt))
    p1 = (exp(r*dt)-d1)/(u1-d1)

    u2 = exp(sig*sqrt(dt)+(r-sig*sig/2)*dt)
    d2 = exp(-sig*sqrt(dt)+(r-sig*sig/2)*dt)
    p2 = (exp(r*dt)-d2)/(u2-d2)

    callp1[i], putp1[i] = getOptionPrice(S0, K[i], T, r, sig, M, p1, u1, d1, dt)
    callp2[i], putp2[i] = getOptionPrice(S0, K[i], T, r, sig, M, p2, u2, d2, dt)

plt.plot(K, callp1, color='red')
plt.title('Plot of Call option at t=0 vs K (50-150) for Set 1 ')
plt.xlabel('Strike Price (K)')
plt.ylabel('Price of Call option')
plt.show()

plt.plot(K, putp1, color='green')
plt.title('Plot of Put option at t=0 vs K (50-150) for Set 1 ')
plt.xlabel('Strike Price (K)')
plt.ylabel('Price of Put option')
plt.show()

plt.plot(K, callp2, color='red')
plt.title('Plot of Call option at t=0 vs K (50-150) for Set 2 ')
plt.xlabel('Strike Price (K)')
plt.ylabel('Price of Call option')
plt.show()

plt.plot(K, putp2, color='green')
plt.title('Plot of Put option at t=0 vs K (50-150) for Set 2 ')
plt.xlabel('Strike Price (K)')
plt.ylabel('Price of Put option')
plt.show()

# (c) r from 0 to 0.2 in steps of 0.01
r = np.arange(0, 0.21, 0.01)
callp1, putp1, callp2, putp2 = np.zeros(r.shape[0]), np.zeros(
    r.shape[0]), np.zeros(r.shape[0]), np.zeros(r.shape[0])

for i in range(r.shape[0]):
    S0, K, T, M, sig = 100, 100, 1, 100, 0.2
    dt = T/M

    u1 = exp(sig*sqrt(dt))
    d1 = exp(-sig*sqrt(dt))
    p1 = (exp(r[i]*dt)-d1)/(u1-d1)

    u2 = exp(sig*sqrt(dt)+(r[i]-sig*sig/2)*dt)
    d2 = exp(-sig*sqrt(dt)+(r[i]-sig*sig/2)*dt)
    p2 = (exp(r[i]*dt)-d2)/(u2-d2)

    callp1[i], putp1[i] = getOptionPrice(S0, K, T, r[i], sig, M, p1, u1, d1, dt)
    callp2[i], putp2[i] = getOptionPrice(S0, K, T, r[i], sig, M, p2, u2, d2, dt)

plt.plot(r, callp1, color='red')
plt.title('Plot of Call option at t=0 vs r (0-0.2) for Set 1 ')
plt.xlabel('Interest Rate (r)')
plt.ylabel('Price of Call option')
plt.show()

plt.plot(r, putp1, color='green')
plt.title('Plot of Put option at t=0 vs r (0-0.2) for Set 1 ')
plt.xlabel('Interest Rate (r)')
plt.ylabel('Price of Put option')
plt.show()

plt.plot(r, callp2, color='red')
plt.title('Plot of Call option at t=0 vs r (0-0.2) for Set 2 ')
plt.xlabel('Interest Rate (r)')
plt.ylabel('Price of Call option')
plt.show()

plt.plot(r, putp2, color='green')
plt.title('Plot of Put option at t=0 vs r (0-0.2) for Set 2 ')
plt.xlabel('Interest Rate (r)')
plt.ylabel('Price of Put option')
plt.show()

# (d) sig from 0 to 0.3 in steps of 0.01
sig = np.arange(0.01, 0.31, 0.01)  # avoid getting sigma = 0 exactly as it'd lead to div by 0
callp1, putp1, callp2, putp2 = np.zeros(sig.shape[0]), np.zeros(
    sig.shape[0]), np.zeros(sig.shape[0]), np.zeros(sig.shape[0])

for i in range(sig.shape[0]):
    S0, K, T, M, r = 100, 100, 1, 100, 0.08
    dt = T/M

    u1 = exp(sig[i]*sqrt(dt))
    d1 = exp(-sig[i]*sqrt(dt))
    p1 = (exp(r*dt)-d1)/(u1-d1)

    u2 = exp(sig[i]*sqrt(dt)+(r-sig[i]*sig[i]/2)*dt)
    d2 = exp(-sig[i]*sqrt(dt)+(r-sig[i]*sig[i]/2)*dt)
    p2 = (exp(r*dt)-d2)/(u2-d2)

    callp1[i], putp1[i] = getOptionPrice(S0, K, T, r, sig[i], M, p1, u1, d1, dt)
    callp2[i], putp2[i] = getOptionPrice(S0, K, T, r, sig[i], M, p2, u2, d2, dt)

plt.plot(sig, callp1, color='red')
plt.title('Plot of Call option at t=0 vs sig (0-0.3) for Set 1 ')
plt.xlabel('Value of Sigma')
plt.ylabel('Price of Call option')
plt.show()

plt.plot(sig, putp1, color='green')
plt.title('Plot of Put option at t=0 vs sig (0-0.3) for Set 1 ')
plt.xlabel('Value of Sigma')
plt.ylabel('Price of Put option')
plt.show()

plt.plot(sig, callp2, color='red')
plt.title('Plot of Call option at t=0 vs sig (0-0.3) for Set 2 ')
plt.xlabel('Value of Sigma')
plt.ylabel('Price of Call option')
plt.show()

plt.plot(sig, putp2, color='green')
plt.title('Plot of Put option at t=0 vs sig (0-0.3) for Set 2 ')
plt.xlabel('Value of Sigma')
plt.ylabel('Price of Put option')
plt.show()

# (e) M from 50-150 in steps of 1, and K for 3 values 95, 100 and 105
K = np.arange(95, 110, 5)
M = np.arange(50, 151, 1)

for j in range(K.shape[0]):
    callp1, putp1, callp2, putp2 = np.zeros(M.shape[0]), np.zeros(
        M.shape[0]), np.zeros(M.shape[0]), np.zeros(M.shape[0])

    for i in range(M.shape[0]):
        S0, T, r, sig = 100, 1, 0.08, 0.2
        dt = T/M[i]

        u1 = exp(sig*sqrt(dt))
        d1 = exp(-sig*sqrt(dt))
        p1 = (exp(r*dt)-d1)/(u1-d1)

        u2 = exp(sig*sqrt(dt)+(r-sig*sig/2)*dt)
        d2 = exp(-sig*sqrt(dt)+(r-sig*sig/2)*dt)
        p2 = (exp(r*dt)-d2)/(u2-d2)

        callp1[i], putp1[i] = getOptionPrice(S0, K[j], T, r, sig, M[i], p1, u1, d1, dt)
        callp2[i], putp2[i] = getOptionPrice(S0, K[j], T, r, sig, M[i], p2, u2, d2, dt)

    plt.plot(M, callp1, color='red')
    plt.title('Plot of Call option at t=0 vs M (50-150) for Set 1 and K = {} '.format(K[j]))
    plt.xlabel('No. of Steps (M)')
    plt.ylabel('Price of Call option')
    plt.show()

    plt.plot(M, putp1, color='green')
    plt.title('Plot of Put option at t=0 vs M (50-150) for Set 1 and K = {} '.format(K[j]))
    plt.xlabel('No. of Steps (M)')
    plt.ylabel('Price of Put option')
    plt.show()

    plt.plot(M, callp2, color='red')
    plt.title('Plot of Call option at t=0 vs M (50-150) for Set 2 and K = {} '.format(K[j]))
    plt.xlabel('No. of Steps (M)')
    plt.ylabel('Price of Call option')
    plt.show()

    plt.plot(M, putp2, color='green')
    plt.title('Plot of Put option at t=0 vs M (50-150) for Set 2 and K = {} '.format(K[j]))
    plt.xlabel('No. of Steps (M)')
    plt.ylabel('Price of Put option')
    plt.show()

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

# Draw 3-D Plots
# We'll have C(5,2) = 10 cases taking 2 parameters at a time and then plotting 4 graphs for each case.

# (a) S0 and K taken together
# Generate 600 uniform discrete random numbers in range 1 - 300
S0 = np.random.randint(1, 300+1, 600)
K = np.random.randint(1, 300+1, 600)

callp1, putp1, callp2, putp2 = np.zeros(S0.shape[0]), np.zeros(
    S0.shape[0]), np.zeros(S0.shape[0]), np.zeros(S0.shape[0])

for i in range(S0.shape[0]):
    T, M, r, sig = 1, 100, 0.08, 0.2
    dt = T/M

    u1 = exp(sig*sqrt(dt))
    d1 = exp(-sig*sqrt(dt))
    p1 = (exp(r*dt)-d1)/(u1-d1)

    u2 = exp(sig*sqrt(dt)+(r-sig*sig/2)*dt)
    d2 = exp(-sig*sqrt(dt)+(r-sig*sig/2)*dt)
    p2 = (exp(r*dt)-d2)/(u2-d2)

    callp1[i], putp1[i] = getOptionPrice(S0[i], K[i], T, r, sig, M, p1, u1, d1, dt)
    callp2[i], putp2[i] = getOptionPrice(S0[i], K[i], T, r, sig, M, p2, u2, d2, dt)

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(S0, K, callp1, c='r')
ax.set_title('Scatter Plot of Initial Option Price with S(0) and K (Set 1)')
ax.set_xlabel('Stock Price at t=0 - S(0)')
ax.set_ylabel('Strike Price (K)')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(S0, K, putp1, c='g')
ax.set_title('Scatter Plot of Initial Option Price with S(0) and K (Set 1)')
ax.set_xlabel('Stock Price at t=0 - S(0)')
ax.set_ylabel('Strike Price (K)')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(S0, K, callp2, c='r')
ax.set_title('Scatter Plot of Initial Option Price with S(0) and K (Set 2)')
ax.set_xlabel('Stock Price at t=0 - S(0)')
ax.set_ylabel('Strike Price (K)')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(S0, K, putp2, c='g')
ax.set_title('Scatter Plot of Initial Option Price with S(0) and K (Set 2)')
ax.set_xlabel('Stock Price at t=0 - S(0)')
ax.set_ylabel('Strike Price (K)')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

# (b) S0 and r taken together
# Generate 600 uniform discrete random numbers in range 1 - 300 and in range 0 to 0.2

S0 = np.random.randint(1, 300+1, 600)
r = np.random.uniform(0, 0.2, 600)

callp1, putp1, callp2, putp2 = np.zeros(S0.shape[0]), np.zeros(
    S0.shape[0]), np.zeros(S0.shape[0]), np.zeros(S0.shape[0])

for i in range(S0.shape[0]):
    K, T, M, sig = 100, 1, 100, 0.2
    dt = T/M

    u1 = exp(sig*sqrt(dt))
    d1 = exp(-sig*sqrt(dt))
    p1 = (exp(r[i]*dt)-d1)/(u1-d1)

    u2 = exp(sig*sqrt(dt)+(r[i]-sig*sig/2)*dt)
    d2 = exp(-sig*sqrt(dt)+(r[i]-sig*sig/2)*dt)
    p2 = (exp(r[i]*dt)-d2)/(u2-d2)

    callp1[i], putp1[i] = getOptionPrice(S0[i], K, T, r[i], sig, M, p1, u1, d1, dt)
    callp2[i], putp2[i] = getOptionPrice(S0[i], K, T, r[i], sig, M, p2, u2, d2, dt)

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(S0, r, callp1, c='r')
ax.set_title('Scatter Plot of Initial Option Price with S(0) and r (Set 1)')
ax.set_xlabel('Stock Price at t=0 - S(0)')
ax.set_ylabel('Interest Rate (r)')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(S0, r, putp1, c='g')
ax.set_title('Scatter Plot of Initial Option Price with S(0) and r (Set 1)')
ax.set_xlabel('Stock Price at t=0 - S(0)')
ax.set_ylabel('Interest Rate (r)')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(S0, r, callp2, c='r')
ax.set_title('Scatter Plot of Initial Option Price with S(0) and r (Set 2)')
ax.set_xlabel('Stock Price at t=0 - S(0)')
ax.set_ylabel('Interest Rate (r)')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(S0, r, putp2, c='g')
ax.set_title('Scatter Plot of Initial Option Price with S(0) and r (Set 2)')
ax.set_xlabel('Stock Price at t=0 - S(0)')
ax.set_ylabel('Interest Rate (r)')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

# (c) S0 and sigma taken together
# Generate 600 uniform discrete random numbers in range 1 - 300 and in range 0 to 0.2

S0 = np.random.randint(1, 300+1, 600)
sig = np.random.uniform(0.1, 0.3, 600)

callp1, putp1, callp2, putp2 = np.zeros(S0.shape[0]), np.zeros(
    S0.shape[0]), np.zeros(S0.shape[0]), np.zeros(S0.shape[0])

for i in range(S0.shape[0]):
    K, T, M, r = 100, 1, 100, 0.08
    dt = T/M

    u1 = exp(sig[i]*sqrt(dt))
    d1 = exp(-sig[i]*sqrt(dt))
    p1 = (exp(r*dt)-d1)/(u1-d1)

    u2 = exp(sig[i]*sqrt(dt)+(r-sig[i]*sig[i]/2)*dt)
    d2 = exp(-sig[i]*sqrt(dt)+(r-sig[i]*sig[i]/2)*dt)
    p2 = (exp(r*dt)-d2)/(u2-d2)

    callp1[i], putp1[i] = getOptionPrice(S0[i], K, T, r, sig[i], M, p1, u1, d1, dt)
    callp2[i], putp2[i] = getOptionPrice(S0[i], K, T, r, sig[i], M, p2, u2, d2, dt)

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(S0, sig, callp1, c='r')
ax.set_title('Scatter Plot of Initial Option Price with S(0) and sig (Set 1)')
ax.set_xlabel('Stock Price at t=0 - S(0)')
ax.set_ylabel('Value of Sigma')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(S0, sig, putp1, c='g')
ax.set_title('Scatter Plot of Initial Option Price with S(0) and sig (Set 1)')
ax.set_xlabel('Stock Price at t=0 - S(0)')
ax.set_ylabel('Value of Sigma')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(S0, sig, callp2, c='r')
ax.set_title('Scatter Plot of Initial Option Price with S(0) and sig (Set 2)')
ax.set_xlabel('Stock Price at t=0 - S(0)')
ax.set_ylabel('Value of Sigma')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(S0, sig, putp2, c='g')
ax.set_title('Scatter Plot of Initial Option Price with S(0) and sig (Set 2)')
ax.set_xlabel('Stock Price at t=0 - S(0)')
ax.set_ylabel('Value of Sigma')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

# (d) S0 and M taken together
# Generate 600 uniform discrete random numbers in range 1 - 300
S0 = np.random.randint(1, 300+1, 600)
M = np.random.randint(1, 300+1, 600)

callp1, putp1, callp2, putp2 = np.zeros(S0.shape[0]), np.zeros(
    S0.shape[0]), np.zeros(S0.shape[0]), np.zeros(S0.shape[0])

for i in range(S0.shape[0]):
    T, K, r, sig = 1, 100, 0.08, 0.2
    dt = T/M[i]

    u1 = exp(sig*sqrt(dt))
    d1 = exp(-sig*sqrt(dt))
    p1 = (exp(r*dt)-d1)/(u1-d1)

    u2 = exp(sig*sqrt(dt)+(r-sig*sig/2)*dt)
    d2 = exp(-sig*sqrt(dt)+(r-sig*sig/2)*dt)
    p2 = (exp(r*dt)-d2)/(u2-d2)

    callp1[i], putp1[i] = getOptionPrice(S0[i], K, T, r, sig, M[i], p1, u1, d1, dt)
    callp2[i], putp2[i] = getOptionPrice(S0[i], K, T, r, sig, M[i], p2, u2, d2, dt)

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(S0, M, callp1, c='r')
ax.set_title('Scatter Plot of Initial Option Price with S(0) and M (Set 1)')
ax.set_xlabel('Stock Price at t=0 - S(0)')
ax.set_ylabel('No. of Steps (M)')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(S0, M, putp1, c='g')
ax.set_title('Scatter Plot of Initial Option Price with S(0) and M (Set 1)')
ax.set_xlabel('Stock Price at t=0 - S(0)')
ax.set_ylabel('No. of Steps (M)')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(S0, M, callp2, c='r')
ax.set_title('Scatter Plot of Initial Option Price with S(0) and M (Set 2)')
ax.set_xlabel('Stock Price at t=0 - S(0)')
ax.set_ylabel('No. of Steps (M)')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(S0, M, putp2, c='g')
ax.set_title('Scatter Plot of Initial Option Price with S(0) and M (Set 2)')
ax.set_xlabel('Stock Price at t=0 - S(0)')
ax.set_ylabel('No. of Steps (M)')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

# (e) K and r taken together
# Generate 600 uniform discrete random numbers in range 1 - 300 and in range 0 to 0.2

K = np.random.randint(1, 300+1, 600)
r = np.random.uniform(0, 0.2, 600)

callp1, putp1, callp2, putp2 = np.zeros(K.shape[0]), np.zeros(
    K.shape[0]), np.zeros(K.shape[0]), np.zeros(K.shape[0])

for i in range(K.shape[0]):
    S0, T, M, sig = 100, 1, 100, 0.2
    dt = T/M

    u1 = exp(sig*sqrt(dt))
    d1 = exp(-sig*sqrt(dt))
    p1 = (exp(r[i]*dt)-d1)/(u1-d1)

    u2 = exp(sig*sqrt(dt)+(r[i]-sig*sig/2)*dt)
    d2 = exp(-sig*sqrt(dt)+(r[i]-sig*sig/2)*dt)
    p2 = (exp(r[i]*dt)-d2)/(u2-d2)

    callp1[i], putp1[i] = getOptionPrice(S0, K[i], T, r[i], sig, M, p1, u1, d1, dt)
    callp2[i], putp2[i] = getOptionPrice(S0, K[i], T, r[i], sig, M, p2, u2, d2, dt)

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(K, r, callp1, c='r')
ax.set_title('Scatter Plot of Initial Option Price with K and r (Set 1)')
ax.set_xlabel('Strike Price (K)')
ax.set_ylabel('Interest Rate (r)')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(K, r, putp1, c='g')
ax.set_title('Scatter Plot of Initial Option Price with K and r (Set 1)')
ax.set_xlabel('Strike Price (K)')
ax.set_ylabel('Interest Rate (r)')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(K, r, callp2, c='r')
ax.set_title('Scatter Plot of Initial Option Price with K and r (Set 2)')
ax.set_xlabel('Strike Price (K)')
ax.set_ylabel('Interest Rate (r)')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(K, r, putp2, c='g')
ax.set_title('Scatter Plot of Initial Option Price with K and r (Set 2)')
ax.set_xlabel('Strike Price (K)')
ax.set_ylabel('Interest Rate (r)')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

# (f) K and sig taken together
# Generate 600 uniform discrete random numbers in range 1 - 300 and in range 0 to 0.3

K = np.random.randint(1, 300+1, 600)
sig = np.random.uniform(0.1, 0.3, 600)

callp1, putp1, callp2, putp2 = np.zeros(K.shape[0]), np.zeros(
    K.shape[0]), np.zeros(K.shape[0]), np.zeros(K.shape[0])

for i in range(K.shape[0]):
    S0, T, M, r = 100, 1, 100, 0.08
    dt = T/M

    u1 = exp(sig[i]*sqrt(dt))
    d1 = exp(-sig[i]*sqrt(dt))
    p1 = (exp(r*dt)-d1)/(u1-d1)

    u2 = exp(sig[i]*sqrt(dt)+(r-sig[i]*sig[i]/2)*dt)
    d2 = exp(-sig[i]*sqrt(dt)+(r-sig[i]*sig[i]/2)*dt)
    p2 = (exp(r*dt)-d2)/(u2-d2)

    callp1[i], putp1[i] = getOptionPrice(S0, K[i], T, r, sig[i], M, p1, u1, d1, dt)
    callp2[i], putp2[i] = getOptionPrice(S0, K[i], T, r, sig[i], M, p2, u2, d2, dt)

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(K, sig, callp1, c='r')
ax.set_title('Scatter Plot of Initial Option Price with K and sig (Set 1)')
ax.set_xlabel('Strike Price (K)')
ax.set_ylabel('Value of Sigma')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(K, sig, putp1, c='g')
ax.set_title('Scatter Plot of Initial Option Price with K and sig (Set 1)')
ax.set_xlabel('Strike Price (K)')
ax.set_ylabel('Value of Sigma')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(K, sig, callp2, c='r')
ax.set_title('Scatter Plot of Initial Option Price with K and sig (Set 2)')
ax.set_xlabel('Strike Price (K)')
ax.set_ylabel('Value of Sigma')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(K, sig, putp2, c='g')
ax.set_title('Scatter Plot of Initial Option Price with K and sig (Set 2)')
ax.set_xlabel('Strike Price (K)')
ax.set_ylabel('Value of Sigma')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

# (g) K and M taken together
# Generate 600 uniform discrete random numbers in range 1 - 300
K = np.random.randint(1, 300+1, 600)
M = np.random.randint(1, 300+1, 600)

callp1, putp1, callp2, putp2 = np.zeros(K.shape[0]), np.zeros(
    K.shape[0]), np.zeros(K.shape[0]), np.zeros(K.shape[0])

for i in range(K.shape[0]):
    T, S0, r, sig = 1, 100, 0.08, 0.2
    dt = T/M[i]

    u1 = exp(sig*sqrt(dt))
    d1 = exp(-sig*sqrt(dt))
    p1 = (exp(r*dt)-d1)/(u1-d1)

    u2 = exp(sig*sqrt(dt)+(r-sig*sig/2)*dt)
    d2 = exp(-sig*sqrt(dt)+(r-sig*sig/2)*dt)
    p2 = (exp(r*dt)-d2)/(u2-d2)

    callp1[i], putp1[i] = getOptionPrice(S0, K[i], T, r, sig, M[i], p1, u1, d1, dt)
    callp2[i], putp2[i] = getOptionPrice(S0, K[i], T, r, sig, M[i], p2, u2, d2, dt)

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(K, M, callp1, c='r')
ax.set_title('Scatter Plot of Initial Option Price with K and M (Set 1)')
ax.set_xlabel('Strike Price (K)')
ax.set_ylabel('No. of Steps (M)')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(K, M, putp1, c='g')
ax.set_title('Scatter Plot of Initial Option Price with K and M (Set 1)')
ax.set_xlabel('Strike Price (K)')
ax.set_ylabel('No. of Steps (M)')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(K, M, callp2, c='r')
ax.set_title('Scatter Plot of Initial Option Price with K and M (Set 2)')
ax.set_xlabel('Strike Price (K)')
ax.set_ylabel('No. of Steps (M)')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(K, M, putp2, c='g')
ax.set_title('Scatter Plot of Initial Option Price with K and M (Set 2)')
ax.set_xlabel('Strike Price (K)')
ax.set_ylabel('No. of Steps (M)')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

# (h) r and sig taken together
# Generate 600 uniform discrete random numbers in range 0 to 0.2 and in range 0 to 0.3

r = np.random.uniform(0, 0.2, 600)
sig = np.random.uniform(0.1, 0.3, 600)

callp1, putp1, callp2, putp2 = np.zeros(r.shape[0]), np.zeros(
    r.shape[0]), np.zeros(r.shape[0]), np.zeros(r.shape[0])

for i in range(r.shape[0]):
    S0, K, T, M = 100, 100, 1, 100
    dt = T/M

    u1 = exp(sig[i]*sqrt(dt))
    d1 = exp(-sig[i]*sqrt(dt))
    p1 = (exp(r[i]*dt)-d1)/(u1-d1)

    u2 = exp(sig[i]*sqrt(dt)+(r[i]-sig[i]*sig[i]/2)*dt)
    d2 = exp(-sig[i]*sqrt(dt)+(r[i]-sig[i]*sig[i]/2)*dt)
    p2 = (exp(r[i]*dt)-d2)/(u2-d2)

    callp1[i], putp1[i] = getOptionPrice(S0, K, T, r[i], sig[i], M, p1, u1, d1, dt)
    callp2[i], putp2[i] = getOptionPrice(S0, K, T, r[i], sig[i], M, p2, u2, d2, dt)

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(r, sig, callp1, c='r')
ax.set_title('Scatter Plot of Initial Option Price with r and sig (Set 1)')
ax.set_xlabel('Interest Rate (r)')
ax.set_ylabel('Value of Sigma')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(r, sig, putp1, c='g')
ax.set_title('Scatter Plot of Initial Option Price with r and sig (Set 1)')
ax.set_xlabel('Interest Rate (r)')
ax.set_ylabel('Value of Sigma')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(r, sig, callp2, c='r')
ax.set_title('Scatter Plot of Initial Option Price with r and sig (Set 2)')
ax.set_xlabel('Interest Rate (r)')
ax.set_ylabel('Value of Sigma')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(r, sig, putp2, c='g')
ax.set_title('Scatter Plot of Initial Option Price with r and sig (Set 2)')
ax.set_xlabel('Interest Rate (r)')
ax.set_ylabel('Value of Sigma')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

# (i) r and M taken together
# Generate 600 uniform discrete random numbers in range 0 to 0.2 and in range 1 to 300

r = np.random.uniform(0, 0.2, 600)
M = np.random.randint(1, 301, 600)

callp1, putp1, callp2, putp2 = np.zeros(r.shape[0]), np.zeros(
    r.shape[0]), np.zeros(r.shape[0]), np.zeros(r.shape[0])

for i in range(r.shape[0]):
    S0, K, T, sig = 100, 100, 1, 0.2
    dt = T/M[i]

    u1 = exp(sig*sqrt(dt))
    d1 = exp(-sig*sqrt(dt))
    p1 = (exp(r[i]*dt)-d1)/(u1-d1)

    u2 = exp(sig*sqrt(dt)+(r[i]-sig*sig/2)*dt)
    d2 = exp(-sig*sqrt(dt)+(r[i]-sig*sig/2)*dt)
    p2 = (exp(r[i]*dt)-d2)/(u2-d2)

    callp1[i], putp1[i] = getOptionPrice(S0, K, T, r[i], sig, M[i], p1, u1, d1, dt)
    callp2[i], putp2[i] = getOptionPrice(S0, K, T, r[i], sig, M[i], p2, u2, d2, dt)

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(r, M, callp1, c='r')
ax.set_title('Scatter Plot of Initial Option Price with r and M (Set 1)')
ax.set_xlabel('Interest Rate (r)')
ax.set_ylabel('No. of Steps (M)')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(r, M, putp1, c='g')
ax.set_title('Scatter Plot of Initial Option Price with r and M (Set 1)')
ax.set_xlabel('Interest Rate (r)')
ax.set_ylabel('No. of Steps (M)')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(r, M, callp2, c='r')
ax.set_title('Scatter Plot of Initial Option Price with r and M (Set 2)')
ax.set_xlabel('Interest Rate (r)')
ax.set_ylabel('No. of Steps (M)')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(r, M, putp2, c='g')
ax.set_title('Scatter Plot of Initial Option Price with r and M (Set 2)')
ax.set_xlabel('Interest Rate (r)')
ax.set_ylabel('No. of Steps (M)')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

# (j) sig and M taken together
# Generate 600 uniform discrete random numbers in range 0 to 0.3 and in range 1 to 300

sig = np.random.uniform(0.1, 0.3, 600)
M = np.random.randint(1, 301, 600)

callp1, putp1, callp2, putp2 = np.zeros(sig.shape[0]), np.zeros(
    sig.shape[0]), np.zeros(sig.shape[0]), np.zeros(sig.shape[0])

for i in range(sig.shape[0]):
    S0, K, T, r = 100, 100, 1, 0.08
    dt = T/M[i]

    u1 = exp(sig[i]*sqrt(dt))
    d1 = exp(-sig[i]*sqrt(dt))
    p1 = (exp(r*dt)-d1)/(u1-d1)

    u2 = exp(sig[i]*sqrt(dt)+(r-sig[i]*sig[i]/2)*dt)
    d2 = exp(-sig[i]*sqrt(dt)+(r-sig[i]*sig[i]/2)*dt)
    p2 = (exp(r*dt)-d2)/(u2-d2)

    callp1[i], putp1[i] = getOptionPrice(S0, K, T, r, sig[i], M[i], p1, u1, d1, dt)
    callp2[i], putp2[i] = getOptionPrice(S0, K, T, r, sig[i], M[i], p2, u2, d2, dt)

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(sig, M, callp1, c='r')
ax.set_title('Scatter Plot of Initial Option Price with sig and M (Set 1)')
ax.set_xlabel('Value of Sigma')
ax.set_ylabel('No. of Steps (M)')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(sig, M, putp1, c='g')
ax.set_title('Scatter Plot of Initial Option Price with sig and M (Set 1)')
ax.set_xlabel('Value of Sigma')
ax.set_ylabel('No. of Steps (M)')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(sig, M, callp2, c='r')
ax.set_title('Scatter Plot of Initial Option Price with sig and M (Set 2)')
ax.set_xlabel('Value of Sigma')
ax.set_ylabel('No. of Steps (M)')
ax.set_zlabel('Price of European Call Option')
ax.invert_yaxis()
plt.show()

fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
ax.scatter(sig, M, putp2, c='g')
ax.set_title('Scatter Plot of Initial Option Price with sig and M (Set 2)')
ax.set_xlabel('Value of Sigma')
ax.set_ylabel('No. of Steps (M)')
ax.set_zlabel('Price of European Put Option')
ax.invert_yaxis()
plt.show()

# -------------------------------------------------------------------------------------------
# Question 1 complete

Semester 6/MA 374 (Financial Engg. Lab)/Lab 2/180123062_AB Satyaprakash_q1.py


from sympy import *
import math

def bisection(f, a, b, eps, eta=1e-16, verbose=False):
    """二分法求根

    对方程 f(x) = 0 在区间 [a, b]，使用二分法求根。
    做 ceil((log(((b - a) / eps), 2) - 1) 次迭代，使结果满足精度 eps。

    实现参考: 数值分析[谷根代，杨晓忠 等著]2011年版.P18.算法1

    Args:
        f: function, 一元函数，表示要求根的方程: f(x) = 0.
        a, b: float, 有根区间 [a, b] 的端点.
        eps: float, 给定精度.

        eta: float, 当 abs(f(x)) <= eta 时停止计算, default 1e-16.
        verbose: bool, 打印出二分法计算的表格, default False.

    Returns:
        (x_final, N)

        x_final: float, 二分法求得的近似根
        N: int, 迭代次数

    Raises:
        ValueError: 给定的 f(a) * f(b) < 0 时无法求解，抛出异常
    """

    if f(a) * f(b) > 0:
        raise ValueError("rootless interval: f(a) * f(b) < 0")

    N = math.ceil((log(((b - a) / eps), 2) - 1).evalf(5))

    if verbose:
        print(f'N = {N}\n')

    x = (a + b) / 2

    if verbose:
        print(f'n \t (a, b) \t f(x_n)')
        print('-'*35)

    for n in range(N+1):
        if abs(f(x)) <= eta:
            break

        if verbose:
            print(f'{n} \t ({a}, {b}) \t f({x})={f(x)}')

        if f(x) * f(a) < 0:
            b = x
        else:
            a = x
        x = (a + b) / 2
        n += 1

    if verbose:
        print(f'{n} \t ({a}, {b}) \t -')

    x_final = (a + b) / 2
    if verbose:
        print(f'\nresult: x = ({a}+{b})/2 = {x_final}')

    return x_final, N+1


from time import time
import matplotlib
#this will prevent the figure from popping up
matplotlib.use('Agg')
from matplotlib import pyplot as plt
import numpy as np
import pycuda.autoinit
from pycuda import gpuarray
from pycuda.elementwise import ElementwiseKernel

# first string: the input, pycuda::complex<float> is a special type from pycuda
# variable "i" is reserved for the thread number
# z(0,0) is the starting point
mandel_ker = ElementwiseKernel(
"pycuda::complex<float> *lattice, float *mandelbrot_graph, int max_iters, float upper_bound",
"""
mandelbrot_graph[i] = 1;

pycuda::complex<float> c = lattice[i];
pycuda::complex<float> z(0,0);

for (int j = 0; j < max_iters; j++)
    {

     z = z*z + c;

     if(abs(z) > upper_bound)
         {
          mandelbrot_graph[i] = 0;
          break;
         }

    }

""",
"mandel_ker")

def gpu_mandelbrot(width, height, real_low, real_high, imag_low, imag_high, max_iters, upper_bound):

    # we set up our complex lattice as such, matrix not supported by pycuda
    real_vals = np.matrix(np.linspace(real_low, real_high, width), dtype=np.complex64)
    imag_vals = np.matrix(np.linspace(imag_high, imag_low, height), dtype=np.complex64) * 1j

    # put them on top of each other
    mandelbrot_lattice = np.array(real_vals + imag_vals.transpose(), dtype=np.complex64)

    # alternative:
    # real = np.linspace(real_low, real_high, width)
    # imag = np.linspace(real_low, real_high, width) * 1j
    # mesh1, mesh2 = np.meshgrid(real, imag)
    # lattice = mesh1 + mesh2

    # copy complex lattice to the GPU
    mandelbrot_lattice_gpu = gpuarray.to_gpu(mandelbrot_lattice)

    # allocate an empty array on the GPU
    mandelbrot_graph_gpu = gpuarray.empty(shape=mandelbrot_lattice.shape, dtype=np.float32)

    mandel_ker(mandelbrot_lattice_gpu, mandelbrot_graph_gpu, np.int32(max_iters), np.float32(upper_bound))

    mandelbrot_graph = mandelbrot_graph_gpu.get()

    return mandelbrot_graph

if __name__ == '__main__':

    t1 = time()
    mandel = gpu_mandelbrot(512,512,-2,2,-2,2,256, 2)
    t2 = time()

    mandel_time = t2 - t1

    t1 = time()
    fig = plt.figure(1)
    plt.imshow(mandel, extent=(-2, 2, -2, 2))
    plt.savefig('mandelbrot.png', dpi=fig.dpi)
    t2 = time()

    dump_time = t2 - t1

    print('It took {} seconds to calculate the Mandelbrot graph.'.format(mandel_time))
    print('It took {} seconds to dump the image.'.format(dump_time))