#!/usr/bin/env python3
# -*- coding: utf-8 -*-

''' polynomial regression using numpy.polynomial.polynomial
It is a form of regression analysis in which
the relationship between the independent variable x
and the dependent variable y is modelled as an
nth degree polynomial in x.

y = c0 * x^0 + c1 * x^1 +
    c2 * x^2 + c3 * x^3 + ..... + cn * x ^n
n = degree
'''

import random
import numpy as np
import matplotlib.pyplot as plt
import numpy.polynomial.polynomial as poly

''' sample polynomial equation with degree=3'''
''' t =    24.0 * x^0
         + 12.0 * x^1
         -  0.5 * x^2
         +  9.0 * x^3
'''

constant     = 24
error_induce = 0.01

def fx1(x1):
    res   = (12.0  * x1)
    error = 0.0
    error = res * random.uniform(-error_induce, error_induce)
    return res + error
def fx2(x2):
    res   = (-0.50 * x2 * x2)
    error = 0.0
    error = res * random.uniform(-error_induce, error_induce)
    return res + error
def fx3(x3):
    res   = (09.0 * x3 * x3 * x3)
    error = 0.0
    error = res * random.uniform(-error_induce, error_induce)
    return res + error

''' data preparation '''
max_sample_value = 50
total_samples    = 800
train_sample_cnt = int((total_samples * 60.0 / 100.0))
test_sample_cnt  = total_samples - train_sample_cnt
train_sample     = np.linspace(1,train_sample_cnt,train_sample_cnt)
test_sample      = np.linspace(train_sample_cnt,total_samples,test_sample_cnt)

y1_samples       = fx1(np.arange(total_samples))
y2_samples       = fx2(np.arange(total_samples))
y3_samples       = fx3(np.arange(total_samples))

t_samples        = y1_samples + y2_samples + y3_samples

t_samples        = t_samples + constant

''' splitting samples into train data and test data '''
t_samples_train, t_samples_test   = np.split(
                                     t_samples,
                                     [train_sample_cnt,])

''' use numpy.polynomial.polynomial for fitting data'''
coefs   = poly.polyfit(np.arange(train_sample_cnt),
                       t_samples_train,
                       3)
ffit    = poly.Polynomial(coefs)

print("intercept_ = ", coefs[0])
print("coef_      = ", coefs[1],coefs[2],coefs[3])

#plt.plot(np.arange(test_sample_cnt), ffit(t_samples_test))
#plt.show()

''' fit a polynomial with above coefficients'''
ffit = np.poly1d(coefs[::-1])

plt.title('Polynomial Regression using numpy.polynomial.polynomial', fontsize=16)
plt.plot(train_sample,t_samples_train)
plt.plot(test_sample,ffit(test_sample))
plt.show()

plt.title('Polynomial Regression using numpy.polynomial.polynomial: Difference between actual and expected', fontsize=16)
plt.plot(np.linspace(1,test_sample_cnt,test_sample_cnt),
         (ffit(test_sample) - t_samples_test))
plt.show()


import numpy as np
from numpy import log, exp
from scipy import linalg as la
import matplotlib.pyplot as plt
plt.ion() # Turn on interactive mode for plotting
from scipy import stats

# Build two matrices...

A = np.zeros((4,4))
B = np.zeros((4,4))

# ... and fill in column by column...

A[0,:]=[1,2,3,0]
A[1,:]=[2,0,2,2]
A[2,:]=[1,-2,1,3]
A[3,:]=[5,1,7,8]

# ... or maybe using some pattern...

for j in np.arange(1,3):
    B[j,j]=2
    B[j,j+1]=-1
    B[j,j-1]=-1

B[0,:]=[2,-1,0,0]
B[3,:]=[0,0,-1,2]

# Define a special print function!

def pr(a,b):
    # A personalized print function which adds extra spaces for a cleaner presentation
    print()
    print(a,b)
    print()

# Let's test some matrix operations:

pr('A = ', A )
pr('B = ', B )
pr('|A| = ', la.det(A) )
pr('AB = ', np.dot(A,B) )
pr('*Element-wise* A*B = ', A*B )
pr('A^T = ', A.T)
pr('A^{-1} = ', la.inv(A) )
pr('A^{-1}A = ', np.dot(la.inv(A),A) )

b=np.ones((4,1))
x=la.solve(A,b)
print('Solution to Ax=[1,1,1,1]^T:')
pr('x = ', x )
pr('Ax = ', np.dot(A,x) )

# Now let's time out how long it takes to perform an LU Decomposition as a function of the matrix size.

import time
Nstart = 200
Nfinal = 1500
Ns = 20
sizes = np.zeros((int((Nfinal-Nstart)/Ns),))
times = np.zeros((int((Nfinal-Nstart)/Ns),))

step = 0
print('Running...')
for N in np.arange(Nstart,Nfinal,Ns):

    A = np.random.rand(N,N)

    start = time.time()
    P,L,U = la.lu(A)
    end  = time.time()

    sizes[step] = N
    times[step] = end - start
    step += 1
print('Completed.')

slope, intercept, r_value, p_value, std_err = stats.linregress(log(sizes[-5:]),log(times[-5:]))
pr('slope = ', slope)

# Plotting results
fig = plt.figure(figsize=(8.5, 8.5))
plt.plot(sizes,times,'o')
plt.plot(sizes,exp(intercept + slope*log(sizes)),'r-')
plt.xlabel('N')
plt.ylabel('Time [s]')
plt.savefig('./Random.png')

print('Figure saved as ./Random.png.')
print('Have a nice day.')


from demos.setup import np, plt
from scipy.integrate import quad

""" Computing Function Inner Products, Norms & Metrics """

# Compute Inner Product and Angle
a, b = -1, 1
f = lambda x: 2 * x**2 - 1
g = lambda x: 4 * x**3 - 3*x
fg = quad(lambda x: f(x) * g(x), a, b)[0]
ff = quad(lambda x: f(x) * f(x), a, b)[0]
gg = quad(lambda x: g(x) * g(x), a, b)[0]
angle = np.arccos(fg / np.sqrt(ff * gg)) * 180 / np.pi
print('\nCompute inner product and angle')
print('\tfg = {:6.4f},   ff = {:6.4f}, gg = {:6.4f},  angle = {:3.2f}'.format(fg, ff, gg, angle))

# Compute Function Norm
a, b = 0, 2
f = lambda x: x**2 - 1
p1, p2 = 1, 2
q1 = quad(lambda x: np.abs(f(x) - g(x)) ** p1, a, b)[0] ** (1 / p1)
q2 = quad(lambda x: np.abs(f(x) - g(x)) ** p2, a, b)[0] ** (1 / p2)
print('\nCompute function norm')
print('\tnorm 1 = {:6.4f},   norm 2 = {:6.4f}'.format(q1, q2))

# Compute Function Metrics
a, b = 0, 2
f = lambda x: x**3 + x**2 + 1
g = lambda x: x**3 + 2
p1, p2 = 1, 2
q1 = quad(lambda x: np.abs(f(x)-g(x)) ** p1, a, b)[0] ** (1 / p1)
q2 = quad(lambda x: np.abs(f(x)-g(x)) ** p2, a, b)[0] ** (1 / p2)
print('\nCompute function metrics')
print('\tnorm 1 = {:6.4f},   norm 2 = {:6.4f}'.format(q1, q2))

# Illustrate function metrics
x = np.linspace(a, b, 200)
plt.figure(figsize=[12, 4])
plt.subplot(1, 2, 1)
plt.plot([0, 2], [0, 0], 'k:', linewidth=4)
plt.plot(x, f(x) - g(x), 'b', linewidth=4, label='f - g')
plt.xlabel('x')
plt.ylabel('y')
plt.xticks([0, 1, 2])
plt.yticks([-1, 0, 1, 2, 3])
plt.title('f - g')

plt.subplot(1, 2, 2)
plt.plot(x, np.abs(f(x) - g(x)), 'b', linewidth=4, label='f - g')
plt.xlabel('x')
plt.ylabel('y')
plt.xticks([0, 1, 2])
plt.yticks([0, 1, 2, 3])
plt.title('|f - g|')

plt.show()

# Demonstrate Pythagorean Theorem
a, b = -1, 1
f = lambda x: 2 * x**2 - 1
g = lambda x: 4 * x**3 -3*x
ifsq = quad(lambda x: f(x) ** 2, a, b)[0]
igsq = quad(lambda x: g(x) ** 2, a, b)[0]
ifplusgsq = quad(lambda x: (f(x) + g(x)) ** 2, a, b)[0]
print('\nDemonstrate Pythagorean Theorem')
print(r'    $\int f^2(x) dx$ = {:6.4f}, $\int g^2(x) dx$ = {:6.4f}'.format(ifsq, igsq))
print(r'    $\int f^2(x) dx + \int g^2(x) dx$ = {:6.4f}, $\int (f+g)^2(x) dx$ = {:6.4f}'.format(ifsq + igsq, ifplusgsq))


import pandas as pd
import numpy as np
import datetime as dt
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from scipy import stats

class Valuation:

    def time_to_maturity(self, t0, T, y=252):
        t0 = pd.to_datetime(t0).date()
        T = pd.to_datetime(T).date()
        return ( np.busday_count(t0, T) / y )

    def ddm(self, d, r, g):
        p = d / (r - g)
        return(p)

    def dcf(self, r, *cf):
        n = 1
        p = 0
        for c in cf:
            p += (c / (1+r)**n)
            n += 1
        return(p)

    def futures_price(self, S, r, d, t0, T):
        #ttm = np.busday_count(t0, T) / 252
        ttm = self.time_to_maturity(t0, T)
        F = S * np.exp((r-d)*ttm)
        return (F)

    def call_price(self, S, K, ttm, r, sigma):
        d1 = ( np.log(S / K) + (r + sigma**2 * 0.5) * ttm ) / ( sigma * np.sqrt(ttm) )
        d2 = ( np.log(S / K) + (r - sigma**2 * 0.5) * ttm ) / ( sigma * np.sqrt(ttm) )
        val = ( S * stats.norm.cdf(d1, 0.0, 1.0) ) - K * np.exp( -r * ttm ) * stats.norm.cdf(d2, 0.0, 1.0)
        return val

    def put_price(self, S, K, ttm, r, sigma):
        d1 = ( np.log(S / K) + (r + sigma**2 * 0.5) * ttm ) / ( sigma * np.sqrt(ttm) )
        d2 = ( np.log(S / K) + (r - sigma**2 * 0.5) * ttm ) / ( sigma * np.sqrt(ttm) )
        val = K * np.exp( -r * ttm ) * stats.norm.cdf(-d2, 0.0, 1.0) - ( S * stats.norm.cdf(-d1, 0.0, 1.0) )
        return val

    def call_delta(self, S, K, ttm, r, sigma):
        d1 = ( np.log(S / K) + (r + sigma**2 * 0.5) * ttm ) / ( sigma * np.sqrt(ttm) )
        val = stats.norm.cdf(d1, 0.0, 1.0)
        return val

    def put_delta(self, S, K, ttm, r, sigma):
        d1 = ( np.log(S / K) + (r + sigma**2 * 0.5) * ttm ) / ( sigma * np.sqrt(ttm) )
        val = stats.norm.cdf(d1, 0.0, 1.0)  - 1
        return val

    def ndx(self, x):
        return ( np.exp( -1 * x**2 * 0.5 ) / np.sqrt(2 * np.pi) )

    def gamma(self, S, K, ttm, r, sigma):
        d1 = ( np.log(S / K) + (r + sigma**2 * 0.5) * ttm ) / ( sigma * np.sqrt(ttm) )
        val = ( self.ndx(d1) ) / ( S * sigma * np.sqrt(ttm) )
        return val

    def call_theta(self, S, K, ttm, r, sigma):
        d1 = ( np.log(S / K) + (r + sigma**2 * 0.5) * ttm ) / ( sigma * np.sqrt(ttm) )
        d2 = ( np.log(S / K) + (r - sigma**2 * 0.5) * ttm ) / ( sigma * np.sqrt(ttm) )
        val = -1 * ( ( S * self.ndx(d1) * sigma ) / ( 2 * np.sqrt(ttm)) ) - r * K * np.exp(-r*ttm) * stats.norm.cdf(d2, 0.0, 1.0)
        return val

    def put_theta(self, S, K, ttm, r, sigma):
        d1 = ( np.log(S / K) + (r + sigma**2 * 0.5) * ttm ) / ( sigma * np.sqrt(ttm) )
        d2 = ( np.log(S / K) + (r - sigma**2 * 0.5) * ttm ) / ( sigma * np.sqrt(ttm) )
        val = -1 * ( ( S * self.ndx(d1) * sigma ) / ( 2 * np.sqrt(ttm)) ) + r * K * np.exp(-r*ttm) * stats.norm.cdf(-1*d2, 0.0, 1.0)
        return val

    def vega(self, S, K, ttm, r, sigma):
        d1 = ( np.log(S / K) + (r + sigma**2 * 0.5) * ttm ) / ( sigma * np.sqrt(ttm) )
        d2 = ( np.log(S / K) + (r - sigma**2 * 0.5) * ttm ) / ( sigma * np.sqrt(ttm) )
        val = ( S * np.sqrt(ttm) * self.ndx(d1) )
        return val

    def implied_vol_call(self, S, K, ttm, r, sigma, C, repeat=100):
        for i in range(repeat):
            sigma = sigma - ( (self.call_price(S, K, ttm, r, sigma) - C) / self.vega(S, K, ttm, r, sigma) )
        return sigma

    def implied_vol_put(self, S, K, ttm, r, sigma, P, repeat=100):
        for i in range(repeat):
            sigma = sigma - ( (self.put_price(S, K, ttm, r, sigma) - P) / self.vega(S, K, ttm, r, sigma) )
        return sigma

class ValueAtExpiry:

    def stock(self, x, x0):
        y = x - x0
        return y

    def x_axis(self, x):
        return x*0

    def futures(self, s, k):
        return s - k

    def call_option(self, s, k, p):
        return np.where(s > k, s - k - p, -p)

    def put_option(self, s, k, p):
        return np.where(s < k, k - s - p, -p)

    def ko_put(self, s, k, b, p):
        return np.where(s > b, np.where(s > k, -p, k - s - p), -p)

    def ki_call(self, s, k, b, p):
        return np.where(s > b, np.where(s > k, s - k - p, -p), -p)

    def synthetic(self, x, **y):
        s = pd.Series(0 for _ in range(len(x)))
        for key, value in y.items():
            s = s + pd.Series(value)
        return (s)


# ---------------------------------------------------
# Statistical Thinking in Python (Part 1) - Quantitative exploratory data analysis
# 10 fev 2021
# VNTBJR
# ---------------------------------------------------
#

######################################################################
# Introduction to summary statistics: The sample mean and median  -------------------------------------------
######################################################################
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
sns.set()
df = sns.load_dataset('iris')
versicolor_petal_length = df.petal_length[df['species'] == 'versicolor'].values

# Computing means and medians
# Compute the mean: mean_length_vers
mean_length_vers = np.mean(versicolor_petal_length)
median_length_vers = np.median(versicolor_petal_length)

# Print the result with some nice formatting
print('I. versicolor mean:', mean_length_vers, 'cm')
print('I. versicolor median:', median_length_vers, 'cm')

######################################################################
# Percentiles, outliers, and box plots  -------------------------------------------
######################################################################

# Computing percentiles
# Specify array of percentiles: percentiles
percentiles = np.array([2.5, 25, 50, 75, 97.5])

# Compute percentiles: ptiles_vers
ptiles_vers = np.percentile(versicolor_petal_length, percentiles)

# Print the result
print(ptiles_vers)

# Comparing perceentiles to ECDF
# Plot the ECDF
_ = plt.plot(x_vers, y_vers, '.')
_ = plt.xlabel('petal length (cm)')
_ = plt.ylabel('ECDF')

# Overlay percentiles as red diamonds.
_ = plt.plot(ptiles_vers, percentiles/100, marker = 'D', color = 'red',
         linestyle = 'none')

# Show the plot
plt.show()
plt.clf()

# Box-and-whisker plot
# Create box plot with Seaborn's default settings
_ = sns.boxplot(x = 'species', y = 'petal_length', data = df)

# Label the axes
_ = plt.xlabel('species')
_ = plt.ylabel('peltal length (cm)')

# Show the plot
plt.show()
plt.clf()

######################################################################
# Variance and standard deviation  -------------------------------------------
######################################################################
# Computing the variance
# Array of differences to mean: differences
differences = versicolor_petal_length - np.mean(versicolor_petal_length)

# Square the differences: diff_sq
diff_sq = differences ** 2

# Compute the mean square difference: variance_explicit
variance_explicit = np.mean(diff_sq)

# Compute the variance using NumPy: variance_np
variance_np = np.var(versicolor_petal_length)

# Print the results
print(variance_explicit, variance_np)

# The standard deviation and the variance
# Compute the variance: variance
variance = np.var(versicolor_petal_length)

# Print the square root of the variance
print(np.sqrt(variance))

# Print the standard deviation
print(np.std(versicolor_petal_length))

######################################################################
# Covariance and the Pearason correlation coefficient  -------------------------------------------
######################################################################
# Covariance summarize how one variable varies in relation to another
# It measure how two quantities vary together
# The covariace is the mean of the product of the difference
# between observations and their respective means
# The Pearson correlation coefficient is a dimensionless measure
# of how two variables depend on each other
# We divide the covariance by the product of the standard deviation
# of x and y variables
# It is the comparison of the variability in the data due to
# codependence (the covariance) to the variability inherent to
# each variable indpendently (their standard deviations)

# Scatter plots
versicolor_petal_width = df.petal_width[df['species'] == 'versicolor'].values
# Make a scatter plot
_ = plt.plot(versicolor_petal_length, versicolor_petal_width, marker = '.', linestyle = 'none')

# Label the axes
_ = plt.xlabel('petal length (cm)')
_ = plt.ylabel('petal width (cm)')

# Show the result
plt.show()
plt.clf()

# Computing the covariance
# Compute the covariance matrix: covariance_matrix
covariance_matrix = np.cov(versicolor_petal_length, versicolor_petal_width)

# Print covariance matrix
print(covariance_matrix)

# Extract covariance of length and width of petals: petal_cov
petal_cov = covariance_matrix[0, 1]

# Print the length/width covariance
print(petal_cov)

# Computing the Pearson correlation coefficient
def pearson_r(x, y):
    """Compute Pearson correlation coefficient between two arrays."""
    # Compute correlation matrix: corr_mat
    corr_mat = np.corrcoef(x, y)

    # Return entry [0,1]
    return corr_mat[0,1]

# Compute Pearson correlation coefficient for I. versicolor: r
r = pearson_r(versicolor_petal_length, versicolor_petal_width)

# Print the result
print(r)


# MSDS 400 Module 5 Practice 1

import numpy
from numpy import sin, arange
import matplotlib.pyplot
from matplotlib.pyplot import *

# Students can substitute their own functions to observe convergence
# to limiting values.

def g(x):
    g = (sin(x))  #This is where a student's function can be substituted.
    return g

# An example will be used to show right and left convergence to a value.
# Convergence at x=0 will be shown graphically using g(x).

n=5  # This determines the number of values calculated on each side of x=0.

powers=arange(0,n+1)
denominator=2.0**powers  # denominator contains exponentiated values of 2.0.
delta=2.0           # This is the interval used on either side of the origin.

# The following are values of x and f(x) trending to the limit.
# Delta is being divided by powers of 2 to reduce the distance from the limit.
# The letter "r" denotes from the right, and "l" denotes from the left.

x_r=delta/denominator
y_r=g(x_r)
x_l=-x_r   # The negative sign generates a symmetric point on the left.
y_l=g(x_l)

# The following determines the vertical boundaries of the resulting plot.

ymax = max( abs(y_r) ) + 0.5
ymin = -ymax

figure()
xlim(-delta-0.5,delta+0.5)
ylim(ymin,ymax)

# Plotting is being done in layers.  First the line plot then the points.

plot(x_r,y_r, color='b')
plot(x_l,y_l,color='r')

# The black points were computed.  The yellow point marks the limit.

scatter(x_r,y_r,color='k',s=30)
scatter(x_l,y_l,color='k',s=30)
scatter(0.0,g(0.0),c='y',s=40)

title ('Example of Convergence to a Functional Value')
xlabel('x-axis')
ylabel('y-axis')
show()

# Define a different function.  This one is from Lial Section 11.1 Example 12.
# As x goes to infinity, f tends to 8/3=2.667 (rounded to 3 digits).

def f(x):
    f = (8.0*x)/(3*x-1)
    return f

# The next section shows convergence to a limit at infinity.
# The coding shows list manipulations resulting in a plot.
# For simplicity, equal intervals between calculated points will be used.

number = 210  # This is the number of points calculated (minus the increment).
increment =10  # This is the increment between the points.

y = []
x = []

# The for loop traverses between 10 and 200 in increments of 10.
# A range statement is inclusive of the first number and exclusive of the last.

for k in range(increment, number, increment):
    w=float(k)
    x = x + [k]
    y = y + [f(w)]

print ('Final value equals' , y[-1])  #Floating point with 4 decimals.

figure()
xlim(0,number+increment)
ylim(min(y)-0.1, max(y)+0.1)

# The black points were computed.  The yellow point indicates the limit.

plot(x,y, color='r')
scatter(x,y,color='k',s=30)
scatter(number,y[-1],c='y',s=40)

title ('Example of Convergence to a Limit at Infinity')
xlabel('x-axis')
ylabel('y-axis')
show()  # Plot shows convergence to limit at infinity

# Exercise #1: Refer to Lial Section 11.1 Example 1.  Generalize the code
# for the function indicated to determine a limit when x=2. Compare the
# code and the resulting plot to the answer sheet.

# Exercise #2: Generalize the code which was used to determine a limiting
# value at infinity.  Apply to Lial Section 11.1 Example 11.


# coding: utf-8

# # Introduction
# In this assignment, we analyse signals using the Fast Fourier transform, or the FFT for short. The FFT is a fast implementation of the Discrete Fourier transform(DFT). It runs in $\mathcal{O}(n \log n)$ time complexity. We find the FFTs of various types of signals using the numpy.fft module. We also attempt to approximate the continuous time fourier transform of a gaussian by windowing and sampling in time domain, and then taking the DFT. We iteratively increase window size and number of samples until we obtain an estimate of required accuracy.

# In[1]:

from pylab import *
import numpy as np
import matplotlib.pyplot as plt

# # Spectrum of $\sin^3(t)$

# Using the following identity:
# $$\sin^3(t) = \frac{3}{4}\sin(t) - \frac{1}{4}\sin(3t)$$
#
# We expect two sets of peaks at frequencies of 1 and 3, with heights corresponding to half of $0.75$ and $0.25$.

# In[2]:

x = np.linspace(-4*pi,4*pi,513)[:-1]
w = np.linspace(-64,64,513)[:-1]
y1 = (np.sin(x))**3
Y1 = fftshift(fft(y1))/512
fig,ax = plt.subplots(2)
ax[0].plot(w,abs(Y1))
ax[0].set_xlim([-10,10])
ax[0].set_title(r"Magnitude and Phase plots of DFT of $\sin^3(t)$ ")
ax[0].set_ylabel(r"$|Y|$",size=16)
ax[0].set_xlabel(r"$\omega$",size=16)
ax[0].grid(True)
ii1 = np.where(abs(Y1)<10**-3)
ph = angle(Y1)
ph[ii1] = 0
ax[1].plot(w,ph,"ro")
ax[1].set_xlim([-10,10])
ax[1].grid(True)
ax[1].set_ylabel(r"Phase of $Y$",size=16)
ax[1].set_xlabel(r"$\omega$",size=16)
plt.show()

# We observe the peaks in the magnitude at the expected frequencies of 1 and 3, along with the expected amplitudes. The phases of the peaks are also in agreement with what is expected(one is a positive sine while the other is a negative sine).

# # Spectrum of $\cos^3(t)$

# Using the following identity:
# $$\cos^3(t) = \frac{3}{4}\cos(t) + \frac{1}{4}\cos(3t)$$
#
# We expect two sets of peaks at frequencies of 1 and 3, with heights corresponding to half of $0.75$ and $0.25$.

# In[3]:

x = np.linspace(-4*pi,4*pi,129)[:-1]
w = np.linspace(-16,16,129)[:-1]
y2 = (np.cos(x))**3
Y2 = fftshift(fft(y2))/128
fig,bx = plt.subplots(2)
bx[0].plot(w,abs(Y2))
bx[0].set_xlim([-10,10])
bx[0].grid(True)
bx[0].set_ylabel(r"$|Y|$",size=16)
bx[0].set_xlabel(r"$\omega$",size=16)
bx[0].set_title(r"Magnitude and Phase plots of DFT of $\cos^3(t)$ ")
ii2 = np.where(abs(Y2)>10**-3)
bx[1].plot(w[ii2],angle(Y2[ii2]),"ro")
bx[1].set_xlim([-10,10])
bx[1].grid(True)
bx[1].set_ylabel(r"Phase of $Y$",size=16)
bx[1].set_xlabel(r"$\omega$",size=16)
plt.show()

# We observe the peaks in the magnitude at the expected frequencies of 1 and 3, along with the expected amplitudes. The phases of the peaks are also in agreement with what is expected(both are positive cosines).

# # Freq Modulation

# We find the DFT of the following frequency modulated signal:
#
# $$ \cos(20t +5 \cos(t))$$

# In[4]:

x = np.linspace(-4*pi,4*pi,513)[:-1]
w = np.linspace(-64,64,513)[:-1]
y1 = cos(20*x + 5*cos(x))
Y1 = fftshift(fft(y1))/512
fig,ax = plt.subplots(2)
ax[0].plot(w,abs(Y1))
ax[0].set_xlim([-40,40])
ax[0].grid(True)
ax[0].set_ylabel(r"$|Y|$",size=16)
ax[0].set_xlabel(r"$\omega$",size=16)
ax[0].set_title(r"Magnitude and Phase plots of DFT of $ \cos(20t +5 \cos(t))$ ")
ii1 = np.where(abs(Y1)<10**-3)
ph = angle(Y1)
ph[ii1] = 0
ax[1].plot(w,ph,"ro")
ax[1].set_xlim([-40,40])
ax[1].grid(True)
ax[1].set_ylabel(r"Phase of $Y$",size=16)
ax[1].set_xlabel(r"$\omega$",size=16)
plt.show()

# # Continuous time Fourier Transform of Gaussian

# The fourier transform of a signal $x(t)$ is defined as follows:
#
# $$X(\omega) = \frac{1}{2 \pi} \int_{- \infty}^{\infty} x(t) e^{-j \omega t} dt$$
#
# We can approximate this by the fourier transform of the windowed version of the signal $x(t)$, with a sufficiently large window. Let the window be of size $T$. We get:
#
# $$X(\omega) \approx \frac{1}{2 \pi} \int_{- \frac{T}{2}}^{\frac{T}{2}} x(t) e^{-j \omega t} dt$$
#
# We can write the integral approximately as a Reimann sum:
#
# $$X(\omega) \approx \frac{\Delta t}{2 \pi} \sum_{n = -\frac{N}{2}}^{\frac{N}{2}-1} x(n \Delta t) e^{-j \omega n \Delta t}$$
#
# Where we divide the integration domain into $N$ parts (assume $N$ is even), each of width $\Delta t = \frac{T}{N}$.
#
# Now, we sample our spectrum with a sampling period in the frequency domain of $\Delta \omega = \frac{2 \pi}{T}$, which makes our continuous time signal periodic with period equal to the window size $T$. Our transform then becomes:
#
# $$X(k \Delta \omega) \approx \frac{\Delta t}{2 \pi} \sum_{n = -\frac{N}{2}}^{\frac{N}{2}-1} x(n \Delta t) e^{-j k n \Delta \omega \Delta t}$$
#
# Which simplifies to:
#
# $$X(k \Delta \omega) \approx \frac{\Delta t}{2 \pi} \sum_{n = -\frac{N}{2}}^{\frac{N}{2}-1}
# x(n \Delta t) e^{-j \frac{2 \pi}{N} k n}$$
#
# Noticing that the summation is of the form of a DFT, we can finally write:
#
# $$X(k \Delta \omega) \approx \frac{\Delta t}{2 \pi} DFT \{x(n \Delta t)\}$$
#
# The two approximations we made were:
#
# * The fourier transform of the windowed signal is approximately the same as that of the original.
# * The integral was approximated as a Reimann sum.
#
# We can improve these approximations by making the window size $T$ larger, and by decreasing the time domain sampling period or increasing the number of samples $N$. We implement this in an iterative algorithm in the next part.
#
# The analytical expression of the fourier transform of the gaussian:
#
# $$x(t) = e^{\frac{-t^2}{2}}$$
#
# Was found as:
#
# $$X(j \omega) = \frac{1}{\sqrt{2 \pi}}e^{\frac{-\omega^2}{2}}$$
#
# We also compare the numerical results with the expected analytical expression.

# In[5]:

def ideal(w):
    return (1/np.sqrt(2*pi)) * (exp((-1*w*w)/2))
def tol(N=128,tol=10**-6):
    T = 8*pi
    N = 128
    error = 10**10
    yold =0
    while error>tol:
        x = np.linspace(-T/2,T/2,N+1)[:-1]
        w = pi* np.linspace(-N/T,N/T,N+1)[:-1]
        Y1 = (T/(2*pi*N)) * fftshift(fft(ifftshift(exp(-x*x/2))))
        error = sum(abs(Y1-ideal(w)))
        yold = Y1
        T = T*2
        N = N*2
    print("max error =" + str(error))
    fig,ax = plt.subplots(2)
    ax[0].plot(w,abs(Y1))
    ax[0].set_xlim([-10,10])
    ax[0].grid(True)
    ax[0].set_ylabel(r"$|Y|$",size=16)
    ax[0].set_xlabel(r"$\omega$",size=16)
    ax[0].set_title(r"Magnitude and Phase plots(calculated) of DFT of $ \exp(-t^{2}/2)$ ")
    ii1 = np.where(abs(Y1)<10**-3)
    ph = angle(Y1)
    ph[ii1] =0
    ax[1].plot(w,ph,"r+")
    ax[1].set_xlim([-10,10])
    ax[1].grid(True)
    ax[1].set_ylabel(r"Phase of $Y$",size=16)
    ax[1].set_xlabel(r"$\omega$",size=16)
    plt.show()
    fig2,bx = plt.subplots(2)
    bx[0].plot(w,abs(ideal(w)))
    bx[0].set_xlim([-10,10])
    bx[0].grid(True)
    bx[0].set_ylabel(r"$|Y|$",size=16)
    bx[0].set_xlabel(r"$\omega$",size=16)
    bx[0].set_title(r"Magnitude and Phase plots(ideal) of DFT of $ \exp(-t^{2}/2)$ ")
    bx[1].plot(w,angle(ideal(w)),"r+")
    bx[1].set_xlim([-10,10])
    bx[1].grid(True)
    bx[1].set_ylabel(r"Phase of $Y$",size=16)
    bx[1].set_xlabel(r"$\omega$",size=16)
    plt.show()

# In[6]:

tol()

# # Conclusions
# * From the above pairs of plots, it is clear that with a sufficiently large window size and sampling rate, the DFT approximates the CTFT of the gaussian.
# * This is because the magnitude of the gaussian quickly approaches $0$ for large values of time. This means that there is lesser frequency domain aliasing due to windowing. This can be interpreted as follows:
# * Windowing in time is equivalent to convolution with a sinc in frequency domain. A large enough window means that the sinc is tall and thin. This tall and thin sinc is approximately equivalent to a delta function for a sufficiently large window. This means that convolution with this sinc does not change the spectrum much.
# * Sampling after windowing is done so that the DFT can be calculated using the Fast Fourier Transform. This is then a sampled version of the DTFT of the sampled time domain signal. With sufficiently large sampling rates, this approximates the CTFT of the original time domain signal.
# * This process is done on the gaussian and the results are in agreement with what is expected.


import numpy as np

def to_polar(x, y):
	"""
	Take two numbers or np arrays and return polar representation
	of the comblex number(s) x + Iy
	Return:
		tuple (r, arg)
	"""
	r = np.sqrt(x**2 + y**2)
	arg = np.arctan2(y, x)
	return r, arg

def to_cartisian(r, theta):
	"""
	Take two numbers or np arrays and return cartisian representation
	of the comblex number(s) r*exp(i*theta)
	Return:
		tuple (x, y)
	"""
	x = r*np.cos(theta)
	y = r*np.sin(theta)
	return (x, y)

######################## FUNCTION DEFINITIONS #################################

def complex_log(r, theta, polar=True):
	"""
	returns complex logarithm as an ordered pair (u, v)
	for a result u + Iv in complex plane.
	Input:
		If polar is True, then input (r, theta) is represented as
		r*e^(i*theta) else the complex representation is taken as r + I*theta
	"""
	if not polar:
		r, theta = to_polar(r, theta)
	u = np.log(r)
	v = theta
	return u, v

def complex_sqrt(r, theta, polar=True):
	"""
	returns complex square root as an ordered pair (u, v)
	for a result u + Iv in complex plane
	Input:
		If polar is True, then input is (r, theta)
		else input is (x, y)
	"""
	if not polar:
		r, theta = to_polar(r, theta)
	u = np.sqrt(r)*np.cos(theta/2)
	v = np.sqrt(r)*np.sin(theta/2)
	return u, v

def f1(r, theta, polar=True):
	"""
	f(z) = sqrt(z**2 + 1)
	"""
	if not polar:
		r, theta = to_polar(r, theta)
	x = r**2 * np.cos(2*theta) + 1
	y = r**2 * np.sin(2*theta)
	u, v = complex_sqrt(x, y, polar=False)
	return u, v

########################## CONTOUR DEFINITIONS #################################

def contour_circle(x0, y0, r0, theta):
	"""
	returns contour as tuple of numpy arrays
	the length of contour is equal to tat of
	theta. theta should be a numpy array
	The contour is a circle centered at (x0, y0)
	with radius r0.
	Returns:
		ordered pain of cartitian form of complex
		number : (x, y)
	"""
	R = np.sqrt(x0**2 + y0**2)
	angle = np.arctan2(y0, x0)

	x = (R*np.cos(angle) + r0*np.cos(theta))
	y = (R*np.sin(angle) + r0*np.sin(theta))
	return (x, y)

def contour_rectangle(x0, y0, l, b, n):
	"""
	Returns rectangle contour as (x, y)
	Input:
		x0, y0 : coordinate of upper left corner
		l, b : length and breadth
		n : number of samples
	"""
	# sharing n over l and b
	nl = int(n/2 * l/(l+b))
	nb = int(n/2 * b/(l+b)) + 1
	l1x = np.linspace(x0, x0+l, nl)
	l1y = np.ones(nl)*y0
	l2x = l1x[::-1]
	l2y = np.ones(nl)*(y0-b)
	b1x = np.ones(nb)*(x0+l)
	b1y = np.linspace(y0, y0-b, nb)
	b2x = np.ones(nb)*x0
	b2y = b1y[::-1]
	x = np.concatenate([l1x, b1x, l2x, b2x])
	y = np.concatenate([l1y, b1y, l2y, b2y])
	# x = np.concatenate([b1x[nb//2:0:-1], l1x[::-1], b2x[::-1], l2x[::-1], b1x[-1:nb//2 -15:-1]])
	# y = np.concatenate([b1y[nb//2:0:-1], l1y[::-1], b2y[::-1], l2y[::-1], b1y[-1:nb//2 -15:-1]])

	return (x, y)

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


import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
import scipy.linalg as la
from scipy.linalg import svd, norm

def truncated_svd(A,r=None,tol=10**-6):
    '''
    Computes the truncated SVD of A. If r is None or equals the number of nonzero singular values, it is the compact SVD.
    Parameters:
        A: the matrix
        r: the number of singular values to use
        tol: the tolerance for zero
    Returns:
        U - the matrix U in the SVD
        s - the diagonals of Sigma in the SVD
        Vh - the matrix V^H in the SVD
    '''
    #Initialize things
    m,n = A.shape

    #Find eigenvalues and eigenvectors of A^H A
    eigs, vr = la.eig(A.conj().T.dot(A))
    #Find singular values
    sigs = np.sqrt(eigs)
    #Find how many singular values are nonzero
    mask = sigs > tol
    num_eigs = np.sum(mask)
    if (r==None):
        #Return compact SVD
        r=num_eigs
    elif (num_eigs < r):
        print 'less nonzero eigenvalues than given size'
        return
    #Initialize things
    U = np.empty((m,r))
    s = np.zeros(r)
    V = np.empty((n,r))

    #Sort the singular values and only keep the greatest r
    sorted_index = np.argsort(sigs)[::-1]
    sigs = sigs[sorted_index]
    s[:r] = sigs[:r]
    #Keep eigenvectors matching the order of the singular values
    vr = vr[:,sorted_index]

    #Calculate V
    #Only keep the first r columns corresponding to the first r singular values
    V = vr[:,:r]

    #Calculate U
    #The first r columns are 1/sigma*A V_i where V_i is the ith column of V.
    #Only use columns that correspond to the first r singular values
    U = 1./sigs[:r]*A.dot(V[:,:r])

    return U, s, V.conj().T

def svd_approx(A, k):
    '''
    Calculate the best rank k approximation to A with respect to the induced
    2-norm. Use the SVD.
    Inputs:
        A -- array of shape (m,n)
        k -- positive integer
    Returns:
        Ahat -- best rank k approximation to A obtained via the SVD
    '''
    #compute the reduced SVD
    U,s,Vh = svd(A,full_matrices=False)

    #keep only the first k singular values
    S = np.diag(s[:k])

    #reconstruct the best rank k approximation
    return U[:,:k].dot(S).dot(Vt[:k,:])

def plot_svd():
	A = np.array([[3,1],[1,3]])
	U, S, V = truncated_svd(A)
	S = np.diag(S)

	t = np.linspace(0,2*np.pi,100)
	pts = np.array([np.cos(t),np.sin(t)])
	v= V.dot(pts)
	sv = S.dot(v)
	a = U.dot(sv)

	unit_vecs = np.array([[1,0],[0,1]])
	vu = V.dot(unit_vecs)
	svu = S.dot(vu)
	au = U.dot(svu)

	plt.subplot(221)
	plt.plot(pts[0],pts[1],'b')
	plt.plot([0,unit_vecs[0,0]],[0,unit_vecs[1,0]],'g')
	plt.plot([0,unit_vecs[0,1]],[0,unit_vecs[1,1]],'g')
	plt.axis('equal')

	plt.subplot(222)
	plt.plot(v[0],v[1],'b')
	plt.plot([0,vu[0,0]],[0,vu[1,0]],'g')
	plt.plot([0,vu[0,1]],[0,vu[1,1]],'g')
	plt.axis('equal')

	plt.subplot(223)
	plt.plot(sv[0],sv[1],'b')
	plt.plot([0,svu[0,0]],[0,svu[1,0]],'g')
	plt.plot([0,svu[0,1]],[0,svu[1,1]],'g')
	plt.axis('equal')

	plt.subplot(224)
	plt.plot(a[0],a[1],'b')
	plt.plot([0,au[0,0]],[0,au[1,0]],'g')
	plt.plot([0,au[0,1]],[0,au[1,1]],'g')
	plt.axis('equal')
	plt.show()

def lowest_rank_approx(A,e):
    '''
    Calculate the lowest rank approximation to A that has error strictly less than e.
    Inputs:
        A -- array of shape (m,n)
        e -- positive floating point number
    Returns:
        Ahat -- the best rank s approximation of A constrained to have error less than e,
                where s is as small as possible.
    '''
    #calculate the reduced SVD
    U,s,Vh = svd(A,full_matrices=False)

    #find the index of the first singular value less than e
    k = np.where(s<e)[0][0]

    #now recreate the rank k approximation
    S = np.diag(s[:k])
    return U[:,:k].dot(S).dot(Vt[:k,:])

def readimg(filename, channel=None):
    if channel is not None:
        return sp.misc.imread(filename)[:,:,channel]
    else:
        return sp.misc.imread(filename)

def compressSVD(filename, rank, random=False, channel=None):
    img = readimg(filename, channel)

    try:
        isize = img[:,:,0].shape
        colors = [la.svd(img[:,:,i]) for i in range(3)]
    except IndexError:
        isize = img.shape
        plt.gray()
        colors = la.svd(img)

    plt.ion()
    imgc = plt.imshow(img)
    newimg = sp.zeros_like(img)

    rank = range(1,rank+1)

    if random is True:
        sp.random.shuffle(rank)

    for r in rank:
        col_res = hat(colors, r-1, r)
        try:
            #col_res[0] = sp.where(col_res[0]>255, col_res[0], 255)
            #col_res[1] = sp.where(col_res[1]>255, col_res[1], 255)
            #col_res[2] = sp.where(col_res[2]>255, col_res[2], 255)

            #col_res[0] = sp.where(col_res[0]<0, col_res[0], 0)
            #col_res[1] = sp.where(col_res[1]<0, col_res[1], 0)
            #col_res[2] = sp.where(col_res[2]<0, col_res[2], 0)

            newimg[:,:,0] += col_res[0]
            newimg[:,:,1] += col_res[1]
            newimg[:,:,2] += col_res[2]

            ## for ch in range(3):
            ##     newimg[newimg[:,:,ch]<1]=0
            ##     newimg[newimg[:,:,ch]>254]=255

        except IndexError:
            newimg += col_res[0]
            ## newimg[newimg<1]=0
            ## newimg[newimg>254]=255

        imgc.set_data(newimg)
        plt.draw()
    plt.ioff()
    plt.show()

    return newimg

def hat(color_svd, lrank, urank):
    results = []
    if len(color_svd) == 3:
        r = 3
    else:
        r = 1

    for c in range(r):
        U = color_svd[c][0]
        S = sp.diag(color_svd[c][1])
        Vt = color_svd[c][2]
        results.append(U[:,lrank:urank].dot(S[lrank:urank, lrank:urank]).dot(Vt[lrank:urank,:]))

    return results

def matrix_rank(X):
    """Compute the rank of a matrix using the SVD"""

    S = la.svd(X, compute_uv=False)
    tol = S.max()*sp.finfo(S.dtype).eps
    return sum(S>tol)


import numpy as np

from hand_crafted_models.loss_functions import mean_squared_error
from hand_crafted_models.optimization import (
    gradient_descent, closed_form_linear_algebra, GradientStep, WeightsAndBias,
)
from hand_crafted_models.utils import forward_pass

def _step(
        x: np.ndarray,
        y: np.ndarray,
        weights: np.ndarray,
        bias: np.ndarray,
        one: np.ndarray
) -> GradientStep:
    """
    Mean-squared-error: Calculate gradients for a given step.

    :param x: Input data [Batch, Features]
    :param y: Label data [Batch, 1]
    :param weights: Feature parameters [1, Features]
    :param bias: Bias parameter [1, 1]
    :param one: (Ignore) Vector of ones
    :return: step loss, weight gradients, bias gradient
    """
    # Get predictions
    y_hat = forward_pass(x=x, weights=weights, bias=bias)
    # Calculate total loss value for current parameter values (e.g., MSE cost fn)
    loss = mean_squared_error(y_hat=y_hat, y=y)
    # Perform back-propagation to parameters
    # The MSE derivative
    d_y = 2 * (y - y_hat)  # [B, 1]
    # The Weights derivative w.r.t. MSE loss fn
    d_w = d_y.T @ x  # [1, B] @ [B, N] -> [1, N]
    # The Bias derivative w.r.t. MSE loss fn
    d_b = d_y.T @ one  # [B, 1] -> [1]
    return loss, d_w, d_b

def get_beta_sgd(
        x: np.ndarray,
        y: np.ndarray,
        lr: float = 0.001,
        tol: float = 1e-6,
        max_grad: float = 10.0,
        max_loops: int = 10000
) -> WeightsAndBias:
    """
    Fit parameters using gradient descent.

    :param x: Input data [Batch, Features]
    :param y: Label data [Batch, 1]
    :param lr: Learning rate (i.e., optimizer step size)
    :param tol: Tolerance for early-stopping
    :param max_grad: (Optional) Max size of gradient
    :param max_loops: Maximum number of steps to take
    :return: weight gradients, bias gradient
    """
    return gradient_descent(
        x=x,
        y=y,
        fn=_step,
        lr=lr,
        tol=tol,
        max_grad=max_grad,
        max_loops=max_loops
    )

def get_beta_linalg(
        x: np.ndarray,
        y: np.ndarray,
        add_bias: bool = True
) -> WeightsAndBias:
    """
    Fit parameters using matrices and linear algebra.

    :param x: Input data [Batch, Features]
    :param y: Label data [Batch, 1]
    :param add_bias: If 'true', append a column of ones to use for the bias
    :return: weight gradients, bias gradient
    """
    return closed_form_linear_algebra(
        x=x,
        y=y,
        add_bias=add_bias
    )


import numpy as np

def sigmoid(x):
    return 1/(1 + np.exp(-x))

def identity_function(x):
    return x

def softmax_ng(a):
    exp_a = np.exp(a)
    sum_exp_a = np.sum(exp_a)
    y = exp_a /sum_exp_a

    return y

def softmax(a):
    c = np.max(a)
    exp_a = np.exp(a - c) # prevent overflow
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a

    return y

def mean_squared_error(y, t):
    return 0.5 * np.sum((y-t)**2)

# def cross_entropy_error(y, t):
#     delta = 1e-7
#     return -np.sum(t * np.log(y + delta))

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    delta = 1e-7
    batch_size = y.shape[0]
    return -np.sum(t * np.log(y + delta)) / batch_size

# def cross_entropy_error(y, t):
#     if y.ndim == 1:
#         t = t.reshape(1, t.size)
#         y = y.reshape(1, y.size)
#
#     batch_size = y.shape[0]
#     return -np.sum(np.log(y[np.arrange(batch_size), t])) / batch_size

def numerical_diff(f, x):
    h = 1e-4
    return (f(x+h) - f(x-h)) / (2*h)


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

def dn_sin(n):
	'''
	Compute the n^th derivative of sin(x) at x=0

	input:
		n - int: the order of the derivative to compute
	output:
		float nth derivative of sin(0)
	purpose:
		problem 1) Write a python function dn_sin0(n) to evaluate the  nth  derivate of  sin(0).
		This should provide a chance to use the if-elif-else control structure.
	'''

	x = 0
	if n%4 == 1: return np.cos(x)
	elif n%4 == 2: return -np.sin(x)
	elif n%4 == 3: return -np.cos(x)
	else: return np.sin(x)
	# pass

def taylor_sin(x, n):
	'''
	Evaluate the Taylor series of sin(x) about x=0 neglecting terms of order x^n

	input:
		x - float: argument of sin
		n - int: number of terms of the taylor series to use in approximation
	output:
		float value computed using the taylor series truncated at the nth term
	'''
	y = 0
	for k in range(n):
		y += dn_sin(k) * (x**k) / math.factorial(k)
	return y
	# pass

def measure_diff(ary1, ary2):
	'''
	Compute a scalar measure of difference between 2 arrays

	input:
		ary1 - numpy array of float values
		ary2 - numpy array of float values
	output:
		a float scalar quantifying difference between the arrays
	'''
	diff = ary1 - ary2
	return np.sum(np.absolute(diff))
	# pass

def escape(cx, cy, dist,itrs, x0=0, y0=0):
	'''
	Compute the number of iterations of the logistic map,
	f(x+j*y)=(x+j*y)**2 + cx +j*cy with initial values x0 and y0
	with default values of 0, to escape from a cirle centered at the origin.

	inputs:
		cx - float: the real component of the parameter value
		cy - float: the imag component of the parameter value
		dist: radius of the circle
		itrs: int max number of iterations to compute
		x0: initial value of x; default value 0
		y0: initial value of y; default value 0
	returns:
		an int scalar interation count
	'''
	x = x0
	y = y0
	r = 0
	for i in range(itrs):
		r = math.sqrt(x**2 + y**2)
		if dist > r:
			x_n = x**2 - y**2 + cx
			y_n = 2*x*y + cy
			x = x_n
			y = y_n
		else: return i
	return 0
	# pass
	#
    # xtemp := x×x - y×y + x0
    # y := 2×x×y + y0
    # x := xtemp
    # iteration := iteration + 1

def mandelbrot(cx,cy,dist,itrs):
	'''
	Compute escape iteration counts for an array of parameter values

	input:
		cx - array: 1d array of real part of parameter
		cy - array: 1d array of imaginary part of parameter
		dist - float: radius of circle for escape
		itrs - int: maximum number of iterations to compute
	output:
		a 2d array of iteration count for each parameter value (indexed pair of values cx, cy)
	'''
	#create a 2D numpy array (init to zero) to store n_ss values at each of the m values of r.
	# x = numpy.zeros([m,n_ss])
	f = np.zeros([len(cx), len(cy)])
	for i in range(len(cx)):
		for j in range(len(cy)):
			f[i][j] = escape(cx[i], cy[j], dist, itrs)
	return f
	# pass

if __name__ == '__main__':
	# problem 1
	print('Problem 1')
	# print(dn_sin.__doc__)
	print(dn_sin(1)) # first: 1

	#Problem 2/3
	print('Problem 2/3')
	x = np.linspace(-2, 2, 100)
	for n in range(2,16,2): #for n in len(range(2,16,2)):
		#compute taylor series
		y = []
		for i in range(len(x)):
			x_i = x[i]
			y.append(taylor_sin(x_i, n)) #collect list of y in n's row
		#plot taylor series
		plt.plot(x, y, label = n) # colect plot for n_th terms
	plt.plot(x, np.sin(x), label = 'sin(x)')
	plt.figure(1)
	plt.legend()
	plt.xlabel('x')
	plt.ylabel('y')
	plt.title('Taylor series (n = num of term)')
	plt.show()

	#Problem 4
	print('Problem 4')
	# diff measure to be less than 1e-2
	# use diff fn.
	# evaluate your functions at 50 points equally spaced across the interval [0,pi/4]
	x = np.linspace(0, np.pi/4, 50)
	y0 = np.sin(x)
	diff = 100
	n = 0
	while diff > 10**-2:
		n += 1
		y = []
		for i in range(len(x)):
			x_i = x[i]
			y.append(taylor_sin(x_i, n))
		diff = measure_diff(y, y0)
	print('current difference = ', diff)
	print('truncation order = ', n)

	# problem 5
	print('Problem 5')
	nx = 512
	ny = 512
	unit_away = 2.5
	i = 256

	x = np.linspace(-2, 1, nx)
	y = np.linspace(-1.5, 1.5, ny)
	# print time required to comput 512 x 512
	t0 = time.time()
	f = mandelbrot(x, y, unit_away, i)
	t1 = time.time() - t0
	print('Time took for calcualting mandelbrot (second) = ', t1)
	print("1(yellow) part is where it can escape.")
	print('0(purbpe) part where it cannot escape')

	for i in range(len(x)):
		for j in range(len(y)):
			if f[i][j] > 0: f[i][j] = 1

	# plot
	plt.figure(2)
	plt.imshow(f.T, extent = (-2.5, 2.5, -2.5, 2.5))
	plt.xlabel('')
	plt.ylabel('')
	plt.title('Mandelbrot graph')
	plt.show()
	# plt.colorbar()

	# A map is a function: it takes an input value (and possibly paramter values) and returns an output value
	# rewrite the map


# when use t test?
# two independent group
# sample not larger than 30
import numpy as np
from scipy import stats

def t_value(X1, X2):
    """
            (n1-1)*s1_var + (n2-1)*s2_var
    temp1 = --------------------------------
                    n1 + n2 - 2

              n1 + n2
    temp2 = ------------
              n1 * n2

            X1_bar - X2_bar
    t = ------------------------
          sqrt(temp1 * temp2)
    """
    X1_bar = np.mean(X1)
    X2_bar = np.mean(X2)
    s1_var = np.var(X1)
    s2_var = np.var(X2)
    n1 = X1.shape[0]
    n2 = X2.shape[0]
    temp1 = ((n1 - 1) * s1_var + (n2 - 1) * s2_var) / (n1 + n2 - 2)
    temp2 = (n1 + n2) / (n1 * n2)
    t = (X1_bar - X2_bar) / np.sqrt(temp1 * temp2)
    return t

def t_value_test():
    X1 = np.array([
        7, 5, 5, 3, 4, 7, 3, 6, 1, 2,
        10, 9, 3, 10, 2, 8, 5, 5, 8, 1,
        2, 5, 1, 12, 8, 4, 15, 5, 3, 4,
    ])
    X2 = np.array([
        5, 3, 4, 4, 2, 3, 4, 5, 2, 5,
        4, 7, 5, 4, 6, 7, 6, 2, 8, 7,
        8, 8, 7, 9, 9, 5, 7, 8, 6, 6,
    ])
    t = t_value(X1, X2)
    print("t_58 = %.2f, p > .05" % t)

    # so t = -0.14
    # what's the threshold when significant level is 5%?
    left, right = stats.t.interval(0.95, df=58)
    print('confident interval [%.3f, %.3f]' % (left, right))
    if left < t < right:
        msg = ("t value live in the confident interval, "
            "accept the original hypothesis")
    else:
        msg = ('t value live out of confident interval, '
            'reject the original hypothesis')
    print(msg)

def effect_size(X1, X2):
    """
               X1_bar - X2_bar
    ES = ---------------------------
         sqrt[(X1_var + X2_var) / 2]
    """
    X1_bar = np.mean(X1)
    X2_bar = np.mean(X2)
    X1_var = np.var(X1)
    X2_var = np.var(X2)
    ES = (X1_bar - X2_bar) / np.sqrt((X1_var + X2_var) / 2)
    return ES

if __name__ == '__main__':
    t_value_test()


import numpy as np
import matplotlib.pyplot as plt

def myf(x):
    return np.exp(2*x)

def myf3derivative(x):
    return 6*np.exp(2*x)

def delta(f,x,m,h):
    if m> 1:
        return (delta(f, x+h/2, m-1, h) - delta(f, x-h/2, m-1, h))/(h)
    else:
        return (f(x+(h/2))-f(x-(h/2)))/h

def centralDiffErr(f,x,C,h):
    return ((2*C*np.abs(f(x)))/h) + (1/24)*(h**2)*(np.abs(delta(myf,x,3,h)))

def optimalError(f,f3,x,C):
    return ((9/8)*(C**2)*(f(x)**2)*f3(x))**(1/3)

#creating a list of number that increase by a factor of 10
hRange = [10**i for i in range(-16,1)]

#using delta to find the first derivative approximation at x = 0
#using central difference method given on the lab 3 document.
#Then find the absolute difference from known solution, 2.
diffFrom2 = [np.abs(delta(myf, 0, 1, i) - 2) for i in hRange]

#graphing part a
fig1, graph = plt.subplots(figsize=(6, 4), dpi=80)
graph.plot(np.arange(-16,1), diffFrom2)
graph.set(xlabel="Power of h (step size)", ylabel="Absolute difference from 2",
       title="Absolute error of each \nderivative over step size")
graph.grid()
fig1.savefig("q3_A.png")

#constant taken from lab manuel
C1 = 10**-16
#calculations for part (a)
print("centtal difference error calculation from textbook",[centralDiffErr(myf, 0, C1, i) for i in hRange])
print("value of h that produced the least difference from 2:", hRange[diffFrom2.index(np.min(diffFrom2))])
print("Min difference from our calculation:", np.min(diffFrom2))
print("Optimal error from equation 5.101:", optimalError(myf, myf3derivative, 0, C1))

#Part B
def cauchy(f,z,m,N):
    coefficients = (float(np.math.factorial(m)))/N
    summation = 0
    for k in range(0, N):
        zk = np.exp(1j*2*np.pi*k/N)
        summation += f(zk)*np.exp(-1j*2*np.pi*k*m/N)
    return (coefficients*summation).real

#Setting up constants and ranges
N = 10000 #number of summation for Cauchy formula
correctDerivative = np.asarray([2**m for m in range(1,11)]) #list of correct result of m-th derivatives
centralDiffResults = np.abs(np.asarray([delta(myf,0,m,0.1) for m in range(1,11)]) - correctDerivative)
cauchyResults = np.abs(np.asarray([cauchy(myf, 0, m, N) for m in range(1,11)]) - correctDerivative)

#graphing part b
fig2, graph = plt.subplots(figsize=(6, 4), dpi=80)
graph.plot(np.arange(1,11), centralDiffResults)
graph.set(xlabel="mth derivative (step size)", ylabel="Absolute difference",
       title="Absolute error of each derivative using\n Central Difference over step size")
graph.grid()
fig2.savefig("q3_B_1.png")

fig3, graph = plt.subplots(figsize=(6, 4), dpi=80)
graph.plot(np.arange(1,11), cauchyResults)
graph.set(xlabel="mth derivative (step size)", ylabel="absolute difference",
       title="Absolute error of each derivative using\n Cauchy Formula over step size")
graph.grid()
fig3.savefig("q3_B_2.png")


import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0,3,100)
y=x**3 -2*x**2 -x + 2

fig = plt.figure()
axdef = fig.add_subplot(1, 1, 1)
axdef.spines['left'].set_position('center')
axdef.spines['bottom'].set_position('zero')
axdef.spines['right'].set_color('none')
axdef.spines['top'].set_color('none')
axdef.xaxis.set_ticks_position('bottom')
axdef.yaxis.set_ticks_position('left')

plt.plot(x,y, 'r')
plt.show()

print('Value of x at the minimum of the function', x[np.argmin(y)])

FirstDerivative = lambda x: 3*x**2-4*x -1
SecondDerivative = lambda x: 6*x-4

ActualX = 3
PrecisionValue = 0.000001
PreviousStepSize = 1
MaxIteration = 10000
IterationCounter = 0

while PreviousStepSize > PrecisionValue and IterationCounter < MaxIteration:
    PreviousX = ActualX
    ActualX = ActualX - FirstDerivative(PreviousX)/ SecondDerivative(PreviousX)
    PreviousStepSize = abs(ActualX - PreviousX)
    IterationCounter = IterationCounter+1
    print("Number of iterations = ",IterationCounter,"\nActual value of x  is = ",ActualX)

print("X value of f(x) minimum = ", ActualX)


import numpy as np

### Functions for you to fill in ###

def polynomial_kernel(X, Y, c, p):
    """
        Compute the polynomial kernel between two matrices X and Y::
            K(x, y) = (<x, y> + c)^p
        for each pair of rows x in X and y in Y.

        Args:
            X - (n, d) NumPy array (n datapoints each with d features)
            Y - (m, d) NumPy array (m datapoints each with d features)
            c - a coefficient to trade off high-order and low-order terms (scalar)
            p - the degree of the polynomial kernel

        Returns:
            kernel_matrix - (n, m) Numpy array containing the kernel matrix
    """
    return (np.dot(X, Y.transpose()) + c)**p

def rbf_kernel(X, Y, gamma):
    """
        Compute the Gaussian RBF kernel between two matrices X and Y::
            K(x, y) = exp(-gamma ||x-y||^2)
        for each pair of rows x in X and y in Y.

        Args:
            X - (n, d) NumPy array (n datapoints each with d features)
            Y - (m, d) NumPy array (m datapoints each with d features)
            gamma - the gamma parameter of gaussian function (scalar)

        Returns:
            kernel_matrix - (n, m) Numpy array containing the kernel matrix
    """
    # TODO: Work on the pairwise distance
    # dist = (X.reshape((X.shape[0], X.shape[1], 1)) - Y.transpose()).sum(axis=1)

    # L2 Vectorized distance between matrices
    dist = -2 * np.dot(X, Y.transpose()) + np.sum(Y**2,
                                                  axis=1) + np.sum(X**2, axis=1)[:, np.newaxis]
    return np.exp(-gamma*dist)

Projects/Digit Recognition (Project 2-3)/part1/kernel.py


# -*- coding: utf-8 -*-
"""
European call option by Monte Carlo simulation

test for vectorized calculation

@author: Minhyun Yoo
"""
import time
import numpy as np
from math import exp, sqrt, log
from scipy import stats
def exc_call(S0, E, T, r, sig):
    d1 = (log(S0/E) + (r + 0.5*sig**2)*T) / (sig*sqrt(T));
    d2 = d1 - sig * sqrt(T);

    call = ( S0 * stats.norm.cdf(d1, 0.0, 1.0)
        - E * exp(-r * T) * stats.norm.cdf(d2, 0.0, 1.0) )

    print 'Exact call Price : %.5f\n' % call

def mc_call(S0, E, T, r, sig, numSim, numStep):

    dt = T / numStep; # time step
    accum = 0.0; # accumulated payoff
    t0 = time.clock();
    for i in range(numSim): # simulation loop
        s = S0;
        for j in range(numStep): # timestep loop
            z = np.random.normal();
            # Geometric Brownian motion
            s = s * exp((r - 0.5*sig**2)*dt + sig*sqrt(dt)*z);
        payoff = max(s - E, 0); # vanilla call
        accum = accum + payoff; # accumulate payoff
    call = exp(-r * T) * (accum / numSim); # expectation
    t1 = time.clock();

    print 'version 0'
    print 'Call Price : %.5f' % call
    print 'CPU time in Python(sec) : %.4f' % (t1-t0)

def mc_call_vec1(S0, E, T, r, sig, numSim, numStep):
    dt = T / numStep;
    accum = 0.0;
    t0 = time.clock();
    z = np.random.normal(size = [numSim, numStep]);
    for i in range(numSim):
        s = S0;
        for j in range(numStep):
            s = s * exp((r - 0.5*sig**2)*dt + sig*sqrt(dt)*z[i, j]);
        payoff = max(s - E, 0);
        accum = accum + payoff;
    call = exp(-r * T) * (accum / numSim);
    t1 = time.clock();

    print 'version 1'
    print 'Call Price : %.5f' % call
    print 'CPU time in Python(sec) : %.4f' % (t1-t0)

def mc_call_vec2(S0, E, T, r, sig, numSim, numStep):
    dt = T / numStep;
    t0 = time.clock();
    z = np.random.normal(size = [numStep, numSim]);
    s = S0 * np.ones([numSim]);
    for i in range(numStep):
        s[:] = s[:] * np.exp((r - 0.5*sig**2)*dt + sig*sqrt(dt)*z[i, :]);
    payoff = np.maximum(s - E, 0);
    call = exp(-r * T) * np.mean(payoff);
    t1 = time.clock();

    print 'version 2'
    print 'Call Price : %.5f' % call
    print 'CPU time in Python(sec) : %.4f' % (t1-t0  )

S0 = 100.0; # underlying price
E = 100.0; # strike price
T = 1.0; # maturity
r = 0.03; # riskless interest rate
sig = 0.3; # volatility
ns = 1000000;  # # of simulations
nStep = 1; # # of time steps (In this example, nStep does not have to over 1 due to European option pricing.)

# functions call
exc_call(S0, E, T, r, sig);
mc_call(S0, E, T, r, sig, ns, nStep);
mc_call_vec1(S0, E, T, r, sig, ns, nStep);
mc_call_vec2(S0, E, T, r, sig, ns, nStep);


# -*- coding: utf-8 -*-
"""Convolutions Sidebar.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/github/lmoroney/dlaicourse/blob/master/Course%201%20-%20Part%206%20-%20Lesson%203%20-%20Notebook.ipynb

Let's explore how convolutions work by creating a basic convolution on a 2D Grey Scale image. First we can load the image by taking the 'ascent' image from scipy. It's a nice, built-in picture with lots of angles and lines.
"""

import cv2
import numpy as np
from scipy import misc
i = misc.ascent()

"""Next, we can use the pyplot library to draw the image so we know what it looks like."""

import matplotlib.pyplot as plt
plt.grid(False)
plt.gray()
plt.axis('off')
plt.imshow(i)
plt.show()

"""The image is stored as a numpy array, so we can create the transformed image by just copying that array. Let's also get the dimensions of the image so we can loop over it later."""

i_transformed = np.copy(i)
size_x = i_transformed.shape[0]
size_y = i_transformed.shape[1]

"""Now we can create a filter as a 3x3 array."""

# This filter detects edges nicely
# It creates a convolution that only passes through sharp edges and straight
# lines.

#Experiment with different values for fun effects.
#filter = [ [0, 1, 0], [1, -4, 1], [0, 1, 0]]

# A couple more filters to try for fun!
filter = [ [-1, -2, -1], [0, 0, 0], [1, 2, 1]]
#filter = [ [-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]

# If all the digits in the filter don't add up to 0 or 1, you
# should probably do a weight to get it to do so
# so, for example, if your weights are 1,1,1 1,2,1 1,1,1
# They add up to 10, so you would set a weight of .1 if you want to normalize them
weight  = 1

"""Now let's create a convolution. We will iterate over the image, leaving a 1 pixel margin, and multiply out each of the neighbors of the current pixel by the value defined in the filter.

i.e. the current pixel's neighbor above it and to the left will be multiplied by the top left item in the filter etc. etc. We'll then multiply the result by the weight, and then ensure the result is in the range 0-255

Finally we'll load the new value into the transformed image.
"""

for x in range(1,size_x-1):
  for y in range(1,size_y-1):
      convolution = 0.0
      convolution = convolution + (i[x - 1, y-1] * filter[0][0])
      convolution = convolution + (i[x, y-1] * filter[0][1])
      convolution = convolution + (i[x + 1, y-1] * filter[0][2])
      convolution = convolution + (i[x-1, y] * filter[1][0])
      convolution = convolution + (i[x, y] * filter[1][1])
      convolution = convolution + (i[x+1, y] * filter[1][2])
      convolution = convolution + (i[x-1, y+1] * filter[2][0])
      convolution = convolution + (i[x, y+1] * filter[2][1])
      convolution = convolution + (i[x+1, y+1] * filter[2][2])
      convolution = convolution * weight
      if(convolution<0):
        convolution=0
      if(convolution>255):
        convolution=255
      i_transformed[x, y] = convolution

"""Now we can plot the image to see the effect of the convolution!"""

# Plot the image. Note the size of the axes -- they are 512 by 512
plt.gray()
plt.grid(False)
plt.imshow(i_transformed)
#plt.axis('off')
plt.show()

"""This code will show a (2, 2) pooling. The idea here is to iterate over the image, and look at the pixel and it's immediate neighbors to the right, beneath, and right-beneath. Take the largest of them and load it into the new image. Thus the new image will be 1/4 the size of the old -- with the dimensions on X and Y being halved by this process. You'll see that the features get maintained despite this compression!"""

new_x = int(size_x/2)
new_y = int(size_y/2)
newImage = np.zeros((new_x, new_y))
for x in range(0, size_x, 2):
  for y in range(0, size_y, 2):
    pixels = []
    pixels.append(i_transformed[x, y])
    pixels.append(i_transformed[x+1, y])
    pixels.append(i_transformed[x, y+1])
    pixels.append(i_transformed[x+1, y+1])
    newImage[int(x/2),int(y/2)] = max(pixels)

# Plot the image. Note the size of the axes -- now 256 pixels instead of 512
plt.gray()
plt.grid(False)
plt.imshow(newImage)
#plt.axis('off')
plt.show()


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

def normalize_transformation(points: np.ndarray) -> np.ndarray:
    """
    Compute a similarity transformation matrix that translate the points such that
    their center is at the origin & the avg distance from the origin is sqrt(2)
    :param points: <float: num_points, 2> set of key points on an image
    :return: (sim_trans <float, 3, 3>)
    """
    center = np.array([np.mean(points[:,0],axis=0),np.mean(points[:,1],axis=0)])  # TODO: find center of the set of points by computing mean of x & y
    # dist = np.array([np.sqrt((points[:,0] - center[0])**2 + (points[:,1] - center[1])**2)])  # TODO: matrix of distance from every point to the origin, shape: <num_points, 1>
    dist = np.linalg.norm(points - center, axis=1)
    s = np.sqrt(2)/np.mean(dist)  # TODO: scale factor the similarity transformation = sqrt(2) / (mean of dist)
    sim_trans = np.array([
        [s,     0,      -s * center[0]],
        [0,     s,      -s * center[1]],
        [0,     0,      1]
    ])
    return sim_trans

def homogenize(points: np.ndarray) -> np.ndarray:
    """
    Convert points to homogeneous coordinate
    :param points: <float: num_points, num_dim>
    :return: <float: num_points, 3>
    """
    return np.concatenate((points, np.ones((points.shape[0], 1))), axis=1)

# read image & put them in grayscale
img1 = cv2.imread('../Materials/chapel00.png', 0)  # queryImage
img2 = cv2.imread('../Materials/chapel01.png', 0)  # trainImage

# detect kpts & compute descriptor
orb = cv2.ORB_create()
kp1, des1 = orb.detectAndCompute(img1, None)
kp2, des2 = orb.detectAndCompute(img2, None)

# match kpts
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
matches = bf.match(des1, des2)

# organize key points into matrix, each row is a point
query_kpts = np.array([kp1[m.queryIdx].pt for m in matches]).reshape((-1, 2))  # shape: <num_pts, 2>
train_kpts = np.array([kp2[m.trainIdx].pt for m in matches]).reshape((-1, 2))  # shape: <num_pts, 2>

# normalize kpts
T_query = normalize_transformation(query_kpts)  # get the similarity transformation for normalizing query kpts
normalized_query_kpts = (T_query @ homogenize(query_kpts).T).T # TODO: apply T_query to query_kpts to normalize them

T_train = normalize_transformation(train_kpts)  # get the similarity transformation for normalizing train kpts
normalized_train_kpts = (T_train @ homogenize(train_kpts).T).T  # TODO: apply T_train to train_kpts to normalize them

print(T_query)
print(T_train)
# construct homogeneous linear equation to find fundamental matrix
a1 = (normalized_train_kpts[:,0]*normalized_query_kpts[:,0]).reshape(-1, 1)
a2 = (normalized_train_kpts[:,0]*normalized_query_kpts[:,1]).reshape(-1, 1)
a3 = normalized_train_kpts[:,0].reshape(-1, 1)
a4 = (normalized_train_kpts[:,1]*normalized_query_kpts[:,0]).reshape(-1, 1)
a5 = (normalized_train_kpts[:,1]*normalized_query_kpts[:,1]).reshape(-1, 1)
a6 = normalized_train_kpts[:,1].reshape(-1, 1)
a7 = normalized_query_kpts[:,0].reshape(-1, 1)
a8 = normalized_query_kpts[:,1].reshape(-1, 1)
a9 = np.ones((len(normalized_train_kpts),1))
A = np.concatenate((a1,a2,a3,a4,a5,a6,a7,a8,a9),axis=1) # TODO: construct A according to Eq.(3) in lab subject

# TODO: find vector f by solving A f = 0 using SVD
# hint: perform SVD of A using np.linalg.svd to get u, s, vh (vh is the transpose of v)
# hint: f is the last column of v
u,s,vh = np.linalg.svd(A)
f = vh.T[:,-1]  # TODO: find f

# arrange f into 3x3 matrix to get fundamental matrix F
F = f.reshape(3, 3)
print('rank F: ', np.linalg.matrix_rank(F))  # should be = 3

# TODO: force F to have rank 2
# hint: perform SVD of F using np.linalg.svd to get u, s, vh
# hint: set the smallest singular value of F to 0
# hint: reconstruct F from u, new_s, vh
u,s,vh = np.linalg.svd(F)
index = np.where(s == np.amin(s))
s[-1] = 0
F = u @ np.diag(s) @ vh

assert np.linalg.matrix_rank(F) == 2, 'Fundamental matrix must have rank 2'

# TODO: de-normlaize F
# hint: last line of Algorithme 1 in the lab subject
F = T_train.T @ F @ T_query
F_gt = np.loadtxt('../Materials/chapel.00.01.F')

Fransac, mask= cv2.findFundamentalMat(query_kpts, train_kpts, cv2.FM_RANSAC)

print('ERROR OF F COMPUTED BY 8-POINT ALGORITHM:')
print(F - F_gt)
print('*****')
print('ERROR OF F COMPUTED BY CV RANSAC:')
print(Fransac - F_gt)

## 1.1.1 Practical

# Load F
F= np.loadtxt('../Materials/chapel.00.01.F')

# Get the real epipole from F
u,s,vh= np.linalg.svd(F.T)
e= vh.T[:,-1]
e /= e[-1]

# Choose two correspondances points
plt.imshow(img1, cmap='gray')
x= plt.ginput(2)
x= np.asarray(x)

# Homogenize the selected points
one= np.ones((1,2))
x= np.concatenate((x,one.T),axis=1)

# Compute the epipolar lines by l = F x
l= F @ x.T

# Compute the epipole from the epipolar lines computed
ec= np.cross(l[:,0],l[:,1])
ec /= ec[-1]

print('****')
print('REAL EPIPOLE: ', e)
print('COMPUTED EPIPOLE: ', ec)

# Visualize the epipolar lines computed
x2 = np.array([0, 500])

a2, b2, c2 = l[:,0].ravel()
y2 = -(x2*a2 + c2) / b2
a3, b3, c3 = l[:,1].ravel()
y3 = -(x2*a3 + c3) / b3

img2=cv2.imread('../Materials/chapel01.png')
image=cv2.line(img2,(x2[0],int(np.around(y2.T[0]))),(x2[-1],int(np.around(y2.T[-1]))),(0,255,0),(1))
image=cv2.line(img2,(x2[0],int(np.around(y3.T[0]))),(x2[-1],int(np.around(y3.T[-1]))),(0,255,0),(1))
cv2.imshow('Epipolar lines',image)

# PRESS A KEY TO FINISH INSTEAD OF CLOSING THE WINDOW
cv2.waitKey(0)


from typing import List, Union
from scipy.optimize import OptimizeResult
import numpy as np
from scipy.integrate import solve_ivp

# https://www.idmod.org/docs/hiv/model-seir.html
# https://www.idmod.org/docs/typhoid/model-seir.html
# The SIR model differential equations. (without vital kinetics)
def seir(t: float, y: List[float], N: Union[int, float], beta: float, gamma: float, sigma: float):
    """
    System of ODE for the Susceptible-Exposed-Infected-Recovered model without vital kinetics

    Parameters
    ----------
    t: float
        Time in days
    y: List[float, float, float, float]
        A list with the current values of
        S: float
            The susceptible population
        E: float
            The exposed population
        I: float
            The infected population
        R: float
            The removed population
    N: int
        The total population
    beta: float
        The contact rate of the disease (1/days)
    gamma: float
        The recovery rate of the disease (1/days)
    sigma: float
        The rate of latent individuals becoming infectious in 1/days (average duration of incubation is 1/sigma)

    Returns
    -------
    OptimizeResult:
        The solution
    """
    S, E, I, R = y
    dSdt = -beta * S * I / N
    dEdt = beta * S * I / N - sigma * E
    dIdt = sigma * E - gamma * I
    dRdt = gamma * I
    return [dSdt, dEdt, dIdt, dRdt]

def seir_model(t: np.ndarray, N: Union[int, float], beta: float, gamma: float, sigma: float,
               **kwargs):
    """
    Solves the Susceptible-Exposed-Infected-Removed ODE

    Parameters
    ----------
    t: np.ndarray
        The time (days)
    N: int
        The total population
    beta: float
        The contact rate of the disease (1/days)
    gamma: float
        The recovery rate of the disease (1/days)
    sigma: float
        The rate of latent individuals becoming infectious in 1/days (average duration of incubation is 1/sigma)
    kwargs: keyword arguments
        I0: int
            The initial number of infected individuals
        R0: int
            The initial number of recovered individuals
        E0: float
            The initial number of exposed individuals
    """

    I0 = kwargs.get('I0', 1)
    R0 = kwargs.get('R0', 0)
    E0 = kwargs.get('E0', 1)

    #    # Total population, N.
    #    N = 1000
    #    # Initial number of infected and recovered individuals, I0 and R0.
    #    I0, R0 = 1, 0
    #    # Everyone else, S0, is susceptible to infection initially.
    S0 = N - I0 - R0 - E0
    #    # Contact rate, beta, and mean recovery rate, gamma, (in 1/days).
    #    beta, gamma = 0.2, 1./10
    #    # A grid of time points (in days)

    # Initial conditions vector
    y0 = S0, E0, I0, R0
    # Integrate the SIR equations over the time grid, t.
    # ret = odeint(deriv, y0, t, args=(N, beta, gamma))
    sol = solve_ivp(seir, [np.amin(t), np.amax(t)], y0, t_eval=t,
                    args=(N, beta, gamma, sigma),
                    method='DOP853',
                    dense_output=True)

    return sol


import numpy as np
import matplotlib.pyplot as plt
from tictoc import *
from plot import plot
#from RFA import bisection

def bisection(f,a,b,TOL,NMAX):
    # THIS FUNCTION PRINTS THE a,b,c VALUES FOR EACH ITERATION:

    #Approximates root using Bisection Method
    #In an interval[a,b] with a tolerance TOL
    #|f(c)| < TOL, where m is the midpoint

    #INPUT : f , a , b , TOL , NMAX
    # f : function / polynomial
    # a : interval start
    # b : interval end
    # TOL : tolerance value
    # NMAX : maximum number of iterations

    #Print Table Header:
    print('--------------------------------------------------------------------------')
    print('iter \t\t a \t\t b \t\t c \t\t f(c)        ')
    print('--------------------------------------------------------------------------')
    #satisfy the conditions needed to apply the Bisection Method:
    if np.sign(f(a)) == np.sign(f(b)):
        print("No root found in the given interval!")
        exit()

    for i in range(NMAX):
        #Compute Midpoint
        c = (a+b)/2
        #print line for the table:
        N.append(1+i)
        f_x.append(f(c))
        print(str(1+i)+'\t% 15.12f\t% 15.12f\t% 15.12f\t% 15.12f\t' %(a, b, c, f(c)))
        #Check stopping condition:
        if np.abs(f(c)) < TOL:
            print('------------------------------------------------------------------------')
            print('Root Found: '+str(c))
            break
        #Implement Recursion:
        elif np.sign(f(a)) == np.sign(f(c)):
            #Improvement on a
            a = c
        elif np.sign(f(b)) == np.sign(f(c)):
            #Improvement on b
            b = c

    if i == NMAX -1:
        print("MAX NUMBER OF ITERATIONS REACHED!")
        print('Approximaiton to the Root after max iterations is : '+str(c))
        exit()
f1 = lambda x: x**3 - 3*(x**2) - x + 9
f2 = lambda x: np.exp(x)*(x**3 - 3*(x**2) - x + 9)
a1 = -5
b1 = 0
a2 = -2.5
b2 = 1

N=[]
f_x=[]
tic()
bisection(f1,a1,b1,1.e-10,1000)
toc()
plot(N,f_x,'b',1)

N=[]
f_x=[]
tic()
bisection(f1,a2,b2,1.e-10,1000)
toc()
plot(N,f_x,'r',1)
plt.legend(['(a1,b1)=(-5,0)','(a2,b2)=(-2.5,1)'])
plt.show()

N=[]
f_x=[]
tic()
bisection(f2,a1,b1,1.e-10,1000)
toc()
plot(N,f_x,'b',2)

N=[]
f_x=[]
tic()
bisection(f2,a2,b2,1.e-10,1000)
toc()
plot(N,f_x,'r',2)
plt.legend(['(a1,b1)=(-5,0)','(a2,b2)=(-2.5,1)'])
plt.show()


"""
Cost functions of Machine Learning
"""

import numpy as np

def calculate_mse_cost(y_pred: np.ndarray, y: np.ndarray) -> float:
    """Calculate error for regression model with mean squared error

    Args:
        y_pred (np.ndarray): predicted y value, y^.
        y (np.ndarray): actual y value.

    Returns:
        float: mean squared error cost.
    """
    residual = y_pred - y
    diff_squared = np.square(residual)
    mse_cost = np.mean(diff_squared) / 2
    return float(mse_cost)

def calculate_entropy_cost(y_pred: np.ndarray, y: np.ndarray) -> float:
    """Calculate entropy error for classification model

    Args:
        y_pred (np.ndarray): predicted y value, y^.
        y (np.ndarray): actual y value.

    Returns:
        float: entorpy error cost.
    """

    part_1 = y * np.log(y_pred)

    part_2 = (1 - y) * np.log(1 - y_pred)

    cost = (-1 / y_pred.shape[0]) * np.sum(part_1 + part_2)
    return cost


import numpy as np

def gausseidel(A, b, imax=1000, es100=1.0, lamb=1.):
    n = len(A)
    A = np.array(A, dtype=float)
    b = np.array(b, dtype=float)
    x = np.zeros(n)
    for i in range(n):
        pivot = A[i,i]
        A[i,:] /= pivot
        b[i] /= pivot
    for i in range(n):
        sum = b[i]
        for j in range(n):
            if i != j: sum -= A[i,j]*x[j]
        x[i] = sum
    iter = 1
    while True:
        sentinel = 1
        for i in range(n):
            old = x[i]
            sum = b[i]
            for j in range(n):
                if i != j: sum -= A[i,j]*x[j]
            x[i] = lamb*sum + (1 - lamb)*old
            if sentinel and x[i]:
                ea = abs(1 - old/x[i])
                if ea*100 > es100: sentinel = 0
        iter += 1
        if sentinel or iter >= imax:
            break
    return x

A = ([[-8, 1, -2], [2, -6, -1], [-3, -1, 7]])
b = ([-20, -38, -34])

print("A = {}".format(np.array(A)))
print("b = {}".format(np.array(b)))

x = gausseidel(A, b, es100=1.0, lamb=1.)
print("(using gauss-seidel(without relaxation)x = {}".format(x))
y = gausseidel(A, b, es100=0.5, lamb=1.2)
print("(using gauss-seidel(with relaxation)x = {}".format(y))


import numpy as np
import matplotlib.pyplot as plt

info_list = []
def InverseMethod(A, converge_range):

    r, c = A.shape

    if r != c:
        raise Exception("not a square matrix")

    #initialize eigenvectors
    vec_list = []
    lambda_list = []
    diff_list = []

    idx = 0
    vec_init = np.zeros(r)
    vec_init[-1] = 1
    vec_list.append(vec_init)

    #initialize eigenvalues
    lambda_init = vec_init.dot(A.dot(vec_init))
    lambda_list.append(lambda_init)

    diff_init = float("inf")
    diff_list.append(diff_init)

    while diff_list[idx] > converge_range:
        #solve the Aw = v^(k-1) for w=vec_new
        vec_new = np.linalg.solve(A, vec_list[idx])
        #normalize the newly found vec_new=w
        vec_new = vec_new / np.linalg.norm(vec_new)
        #add this to the vector list
        vec_list.append(vec_new)
        lambda_new = vec_new.dot(A.dot(vec_new))
        lambda_list.append(lambda_new)
        diff = np.abs(lambda_new - lambda_list[idx])
        diff_list.append(diff)
        idx = idx + 1

    print_log(idx, vec_list, lambda_list, diff_list)

    #plot lambda_list
    x = [i for i in range(idx+1)]
    if len(x) > 20:
        x = x[:21]
        diff_list = diff_list[:21]

    plt.plot(x, diff_list)
    plt.show()
    plt.savefig("II_difference_list_plot")
    return vec_list[-1], lambda_list[-1]

def print_log(idx, vec_list, lambda_list, diff_list):
    print("Inverse Iteration:")
    print('Number of Iterations:', idx)
    cor_eig_vec = vec_list[idx]
    sm_eig_val = round(lambda_list[idx], 4)
    inv_eig_val = np.reciprocal(sm_eig_val)
    print('Inverse Smallest Eigenvalue:', inv_eig_val) #
    print('Smallest Eigenvalue:', sm_eig_val)
    print('Corresponding Eigenvector:', cor_eig_vec)
    print(diff_list[idx]) #don't really need this, just want to see it

#Examples to run on
B = np.array([[1.5, 0.5], [0.5, 1.5]])
C = np.array([[2, 1], [2, 3]]) # eigenval of smallest magnitude of C is 1
D = np.array([[2, 2, -1], [-5, 9, -3], [-4, 4, 1]]) # eigenvals of D are 3, 4, and 5
E = np.array([[-4, 1, 1], [0, 3, 1], [-2, 0, 15]]) # eigenvals of E are ~ -3.9095, 3.0243 and 14.8852
F = np.array([[-6, 3], [4, 5]]) #eigenval of smallest magnitude of F is 6 with corresponding eigenvec [1, 4]
G = np.array([[0.8, 0.3], [0.2, 0.7]])
InverseMethod(D, 0.0001)


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

# Plots Histogram along side acutal distribution
def plotHistogram(sample, a1, a2, N):
	hist, bins, _ = plt.hist(sample, density=True, bins=100)
	xAxis = (bins[1:]+bins[:-1])/2
	yAxis = []
	for x in xAxis:
		yAxis.append(fx(x, a1, a2))

	plt.plot(xAxis, yAxis, label = "f(x)")
	plt.legend(loc = "upper right")
	plt.xlabel('x')
	plt.ylabel('y')
	plt.show()

# Computes f(x)
def fx(x, a1, a2):
	res = (x**(a1-1))*((1-x)**(a2-1))
	res /= beta(a1, a2)
	return res

# Computes B(a1, a2)
def beta(a1, a2):
	res = (gamma(a1)*gamma(a2))/(gamma(a1+a2))
	return res

# Generates Beta Distribution of N values
def generateBeta(a1, a2, c, N):
	res = []
	for i in range(N):
		while True:
			u1 = np.random.uniform(0.00, 1.00)
			u2 = np.random.uniform(0.00, 1.00)

			if fx(u1, a1, a2) >= c*u2:
				res.append(u1)
				break

	return res

def solve(a1, a2):
	x = (a1-1)/(a1+a2-2)
	print('*x = {}'.format(x))
	c = fx(x, a1, a2)
	print('f(*x) = {}'.format(c))

	res = generateBeta(a1, a2, c, 100)
	plotHistogram(res, a1, a2, 100)

	res = generateBeta(a1, a2, c, 1000)
	plotHistogram(res, a1, a2, 1000)

	res = generateBeta(a1, a2, c, 10000)
	plotHistogram(res, a1, a2, 10000)

	res = generateBeta(a1, a2, c, 100000)
	plotHistogram(res, a1, a2, 100000)

	# res = generateBeta(a1, a2, c, 1000000)
	# plotHistogram(res, a1, a2, 1000000)

def main():
	solve(1.43, 2.76)
	# solve(1.25, 2.0)
	# solve(1.25, 2.75)
	# solve(1.0, 2.0)
	# solve(3.25, 4.75)
	# solve(2.25, 1.75)

if __name__ == '__main__':
	main()


import idx2numpy
from matplotlib import pyplot as plt
import numpy as np

# using idx2numpy to import the MNIST dataset
train_img = idx2numpy.convert_from_file('train-images.idx3-ubyte')
train_lb = idx2numpy.convert_from_file('train-labels.idx1-ubyte')
test_img = idx2numpy.convert_from_file('t10k-images.idx3-ubyte')
test_lb = idx2numpy.convert_from_file('t10k-labels.idx1-ubyte')

print(train_img.shape)
print(train_lb.shape)
print(test_img.shape)
print(test_lb.shape)

# set random seed
np.random.seed(42)
trainsize = 2000  # number of training images we use, selected randomly
randtrain = np.random.choice(train_img.shape[0], trainsize, replace=False)
train_image, train_label = train_img[randtrain], train_lb[randtrain]

np.random.seed(10)
testsize = 10000  # number of testing images we use, selected randomly
randtest = np.random.choice(test_img.shape[0], testsize, replace=False)
test_image, test_label = test_img[randtest], test_lb[randtest]

print(train_image.shape, train_label.shape, test_image.shape, test_label.shape)

trainsize = 2000
matrix = []
for i in range(0, trainsize):
    matrix.append(train_image[i].flatten())

matrix = np.array(matrix).T

print(matrix.shape)  # should be (784, trainsize)

# print(matrix)

# take an (x by k) matrix A (where x is the total number of pixels in an image
# and k is the number of training images) and return a vector m of length x
# containing the mean column vector of A and an (x by k) matrix V that
# contains k eigenvectors of the covariance matrix of A
# (after the mean has been subtracted). These should be sorted in descending
# order by eigenvalue (i.e., V(:,1) is the eigenvector with the largest associated
# eigenvalue) and normalized (i.e., norm(V(:,1)) = 1). You can reshape a vector
# and display it as an image.
def hw1FindEigendigits(Mat):
    mean = np.array(np.mean(Mat, axis=1), ndmin=2).T

    mean_Matrix = np.broadcast_to(mean, (784, trainsize))
    A = (Mat - mean_Matrix) / np.sqrt(trainsize)  # A should be a (784,trainsize) matrix

    if trainsize < 784:
        # compute A^tA
        small_Mat = np.matmul(A.T, A)
        # find the eigenvalues and eigenvectors of A^tA
        mu, v = np.linalg.eig(small_Mat)
    else:
        small_Mat = np.matmul(A, A.T)
        mu, v = np.linalg.eig(small_Mat)

    # print(mu)
    print(v[0].shape)  # should be (trainsize,)
    # print(v[0])

    # sort by eigenvalues in decreasing order
    ls = []
    for i in range(min(trainsize, 784)):
        ls.append((mu[i], v[i]))
    ls = sorted(ls, key=lambda ls: ls[0], reverse=True)
    # print(ls)

    # print(np.matmul(A,ls[0][1]).shape)  # should be (784,)

    V = []
    for i in range(min(trainsize, 784)):
        if trainsize < 784:
            vec = np.matmul(A, ls[i][1])
            norm = np.linalg.norm(vec)
            vec = vec / norm
            # print(np.linalg.norm(vec))  # check the norm, should be 1
            V.append(vec)
        else:
            V.append(ls[i][1])

    V = np.array(V).T
    print(V.shape)  # should be (784,trainsize)

    return mean, V

mean, V = hw1FindEigendigits(matrix)
# print(mean.shape)  # should be (784,1)
# print(mean)

# code for displaying the first eight eigendigits
"""
for i in range(8):
    plt.subplot(2, 4, i + 1)
    img = (V[:, i]).reshape(28, 28)
    plt.title('#{}'.format(i + 1))
    plt.imshow(img)
plt.show()
"""

# print(V.shape)  # should be (784,8)

# create eigenvector space
Omega = []
for i in range(trainsize):
    Omega_i = []
    X_i = matrix[:, i] - mean[:, 0]
    for j in range(min(trainsize, 784)):
        Omega_i.append(np.dot(X_i, V[:, j]))
    Omega.append(Omega_i)

Omega = np.array(Omega)
# print(Omega)  # should be (8,8)

# Define our prediction function, which uses KNN to make predictions
def predict(testsize, k):
    predict_label = []
    for i in range(testsize):
        # Form the eigenvector space of the predicting image
        Omega_test = []
        Y_i = test_image[i].flatten() - mean[:, 0]
        for j in range(min(trainsize, 784)):
            Omega_test.append(np.dot(Y_i, V[:, j]))
        # print(Omega_test)

        # reconstruct some test images with Matrix V and the Omega_test above
        if i < 3 and trainsize < 600:
            recon_img = np.matmul(V, Omega_test).reshape(28, 28)
            print(recon_img.shape)
            plt.imshow(recon_img)
            plt.show()

        # Calculate L2-norm distances
        distance = []
        for j in range(trainsize):
            L2 = np.linalg.norm(np.array(Omega_test) - Omega[j, :])
            distance.append((L2, train_label[j]))
        # print(distance)
        # Sort the distance in increasing order
        distance = sorted(distance, key=lambda distance: distance[0])
        # print(distance)

        # Using KNN to choose the k nearest ones and pick the label that appears the most
        KNN_arr = []
        for i in range(k):
            KNN_arr.append(distance[i][1])
        # print(KNN_arr)  # should only contain the k-nearest labels

        # create "bins" to count the occurrences of different labels and create "order"
        # to count the appearing order of them.
        bin = np.zeros((10,), dtype=int)
        order = bin + 10
        count = 0
        for i in range(k):
            bin[KNN_arr[i]] += 1
            if order[KNN_arr[i]] == 10:
                order[KNN_arr[i]] = count
                count += 1

        # print(bin, order*(-0.1))

        # We subtract the bin count by 0.1 times the appearing order,
        # so that the digit with highest counts and appears first will have
        # the largest value.
        # We use 0.1 here so that the digits appear n times will always
        # have higher "rank" values than those appear (n-1) or fewer times.
        rank = bin - 0.1 * order
        # print(rank)

        # using argmax to find the index of the largest value, which becomes
        # the digit we predict, and assign it to label
        label = np.argmax(rank)
        predict_label.append(label)

    return predict_label

# set our testsize and k (number of nearest neighbors) to make predictions
testsize = 10000
k = 4

# display the first few actual test labels
if trainsize < 600:
    for i in range(3):
        plt.imshow(test_image[i])
        plt.show()

# generate predicting labels
predict_label = np.array(predict(testsize, k))
print("Our Predictions: {}".format(predict_label))

# assign actual labels to test_ans
test_ans = test_label[0:testsize]
print("Actual Labels:   {}".format(test_ans))

# Using "==" and sum to compute the number of correct predictions
right_ones = sum(predict_label == test_ans)
# Calculate accuracy
accuracy = right_ones / testsize
# Print accuracy with corresponding trainsize and testsize
print('Accuracy ({} trainsize, {} testsize): {}'.format(trainsize, testsize, accuracy))


import numpy as np

def findAngle(a, b, c):
    """
    find the angle between three point (return in degrees)
    """
    ba = a - b
    bc = c - b

    cosine_angle = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc))
    angle = np.arccos(cosine_angle)

    return np.degrees(angle)

def determinantBetweenPoint(p1, p2, p3):
    """
    find the determinant between three point
    if > 0 then it is left/upper from p1 and p2
    if = 0 then it is in p1 and p2
    if < 0 then it is right/below from p1 and p2
    """
    x1 = p1[0]
    y1 = p1[1]
    x2 = p2[0]
    y2 = p2[1]
    x3 = p3[0]
    y3 = p3[1]
    return x1*y2 + x3*y1 + x2*y3 - x3*y2 - x2*y1 - x1*y3

def pointDistanceMax(S, P1, Pn):
    """
    find the furthest point from  a line (P1, pn) to a point from array S
    if the distance is the same then maximize the angle
    """
    maxD = 0
    pointMaxD = []
    for i in S:
        d = np.abs(np.cross(Pn-P1, i-P1)/np.linalg.norm(Pn-P1))
        if (d > maxD):
            maxD = d
            pointMaxD = i

        elif (d == maxD):
            # maximize the angle if distance is the same
            dAngle = findAngle(P1, i, Pn)
            maxDAngle = findAngle(P1, pointMaxD, Pn)

            if (dAngle > maxDAngle):
                maxD = d
                pointMaxD = i

    return pointMaxD

def splittedConvex(S, P1, Pn, pivot):
    """
    divide and conquer algorithm using quickHull
    """
    if (len(S) == 0):
        # there's no point in S, then P1 and Pn is convexPoint
        convexPoint.append(
            [np.array([P1[0], Pn[0]]), np.array([P1[1], Pn[1]])])

    # edge case for only one point in S
    elif (len(S) == 1):
        pMax = S[0]
        convexPoint.append(
            [np.array([P1[0], pMax[0]]), np.array([P1[1], pMax[1]])])
        convexPoint.append(
            [np.array([pMax[0], Pn[0]]), np.array([pMax[1], Pn[1]])])

    else:
        # find the max distance between point in S to the line between P1 and Pn
        pointMaxD = pointDistanceMax(S, P1, Pn)

        # define S1 and S2
        S1 = []
        S2 = []
        # split to S1 and S2
        for i in S:
            # if pivot is -1 then it is upside down of the original function
            # multiply with -1 so that the function will work properly
            if (pivot == -1):
                dir = -1
            else:
                dir = 1

            # check if point outside of the left triangle of P1, pointMaxD, and Pn
            if (pointMaxD[0] > i[0]):
                dir *= determinantBetweenPoint(pointMaxD, P1, i)

                if (dir < 0):
                    S1.append(i)

            # check if point outside of the right triangle of P1, pointMaxD, and Pn
            elif (pointMaxD[0] < i[0]):
                dir *= determinantBetweenPoint(Pn, pointMaxD, i)

                if (dir < 0):
                    S2.append(i)

        splittedConvex(S1, P1, pointMaxD, pivot)
        splittedConvex(S2, pointMaxD, Pn, pivot)

def convexHull(listOfPoint):
    """
    main function from convex hull
    """
    # initialize convexPoint
    global convexPoint
    convexPoint = []

    # sort array by the absis
    listOfPoint = listOfPoint[listOfPoint[:, 1].argsort(kind='mergesort')]
    listOfPoint = listOfPoint[listOfPoint[:, 0].argsort(kind='mergesort')]
    # take the minimum of absis as P1
    P1 = listOfPoint[0]
    # take the maximum of absis as Pn
    Pn = listOfPoint[-1]

    # define S1 and S2
    S1 = []
    S2 = []
    # split point by the line of P1 and Pn
    for i in listOfPoint[1:-1]:
        dir = determinantBetweenPoint(Pn, P1, i)
        if (dir < 0):
            S1.append(i)
        elif (dir > 0):
            S2.append(i)

    # divide and conquer for S1 and S2
    splittedConvex(S1, P1, Pn, 1)
    splittedConvex(S2, P1, Pn, -1)
    return convexPoint


import numpy as np
import sympy as sp
from sympy.utilities.lambdify import lambdastr
import itertools
import time

def monte_carlo_integration(integrand, bounds: list, n_sample: int = int(1e6)) -> float:
    """
    Compute the definite integral with a monte carlo integration algorithm
    :param integrand: the function to integrate (lambda or func)
    :param bounds: list of integral bounds ex: [[0, 10], [0, 2], [-10, 10]] (list)
    :param n_sample: number of sample to use (int)
    :return: the integral of the integrand (float)
    """
    np.random.seed(1)
    count_in_curve: int = 0

    def sampler():
        while True:
            yield [np.random.uniform(b0, b1) for [b0, b1] in bounds]

    x_rn0, x_rn1 = next(sampler()), next(sampler())
    f_min: float = np.min([integrand(*x_rn0), integrand(*x_rn1)])
    f_max: float = np.max([integrand(*x_rn0), integrand(*x_rn1)])

    for x in itertools.islice(sampler(), n_sample):
        f: float = integrand(*x)

        # Generate random point
        f_rn: float = np.random.uniform(f_min, f_max)

        # Increase the counter
        count_in_curve += 1 if 0 <= f_rn <= f else 0

        # update the domain size
        f_min = np.min([f_min, f])
        f_max = np.max([f_max, f])

    # Compute the hyper volume v of the statistical box
    v: float = np.prod([b1 - b0 for [b0, b1] in bounds]) * (f_max - f_min)
    return (count_in_curve / n_sample) * v

if __name__ == '__main__':
    bounds_x: list = [0, 9]
    bounds_y: list = [-10, 7]
    bounds_z: list = [0, 10]

    bounds: list = [bounds_x, bounds_y, bounds_z]

    def g(x, y, z):
        return 4*x**3 + y**2 + np.sqrt(z)

    start_time = time.time()

    P: float = monte_carlo_integration(g, bounds, n_sample=int(1e6))
    print(f"P = {P:.5e}")
    print(f"--- elapse time: {time.time() - start_time} s ---")

    x, y, z = sp.symbols("x, y, z")
    f = 4*x**3 + y**2 + sp.sqrt(z)
    print(f"f = {f}")
    f_lambdify = sp.lambdify((x, y, z), f)
    print(f"f_lambdify -> {lambdastr((x, y, z), f)}")

    for n in [int(1e1), int(1e3), int(1e6), int(1e8)]:
        start_time = time.time()
        P: float = monte_carlo_integration(f_lambdify, bounds, n_sample=n)
        print(f"P = {P:.5e} for {n:2e} samples")
        print(f"--- elapse time: {time.time() - start_time} s ---")


import solver.algorithms as alg
import numpy as np

def problem4(t0, tf, NA0, NB0, tauA, tauB, n, returnlist=False):
    """Uses Euler's method to model the solution to a radioactive decay problem where dNA/dt = -NA/tauA and dNB/dt = NA/tauA - NB/tauB.

    Args:
        t0 (float): Start time
        tf (float): End time
        NA0 (int): Initial number of NA nuclei
        NB0 (int): Initial number of NB nuclei
        tauA (float): Decay time constant for NA
        tauB (float): Decay time constant for NB
        n (int): Number of points to sample at
        returnlist (bool) = Controls whether the function returns the list of points or not. Defaults to false

    Returns:
        solution (list): Points on the graph of the approximate solution. Each element in the list has the form (t, array([NA, NB]))

    In the graph, NA is green and NB is blue
    """
    print("Problem 4: ~Radioactive Decay~ dNA/dt = -NA/tauA & dNB/dt = NA/tauA - NB/tauA - NB/tauB")
    N0 = np.array([NA0, NB0])
    A = np.array([[-1/tauA, 0],[1/tauA, -1/tauB]])
    def dN_dt(t, N):
        return A @ N
    h = (tf-t0)/(n-1)
    print("Time step of %f seconds." % h)
    solution = alg.euler(t0, tf, n, N0, dN_dt)
    if returnlist:
        return solution


import numpy as np
from matplotlib import pyplot as plt

np.set_printoptions(precision=6)

def estimateGaussian(X):
    """This function estimates the parameters of a
    Gaussian distribution using the data in X

    [mu sigma2] = estimateGaussian(X),
    The input X is the dataset with each n-dimensional data point in one row
    The output is an n-dimensional vector mu, the mean of the data set
    and the variances sigma^2, an n x 1 vector
    :param X:
    :return:
    """
    # Useful variables
    m, n = X.shape

    mu = X.mean(axis=0)
    sigma2 = ((X - mu) ** 2).mean(axis=0)
    return mu, sigma2

def multivariateGaussian(X, mu, Sigma2):
    """Computes the probability density function of the
    multivariate gaussian distribution.

    p = multivariateGaussian(X, mu, Sigma2) Computes the probability
    density function of the examples X under the multivariate gaussian
    distribution with parameters mu and Sigma2. If Sigma2 is a matrix, it is
    treated as the covariance matrix. If Sigma2 is a vector, it is treated
    as the \sigma^2 values of the variances in each dimension (a diagonal
    covariance matrix)
    :param X:
    :param mu:
    :param Sigma2:
    :return:
    """
    k = len(mu)

    if len(Sigma2.shape) == 1:
        Sigma2 = np.diag(Sigma2)

    X = X - mu.reshape(1, -1)
    p = (2 * np.pi) ** (- k / 2) * np.linalg.det(Sigma2) ** (-0.5) * np.exp(
        -0.5 * np.diag(np.matmul(np.matmul(X, np.linalg.pinv(Sigma2)), X.transpose())))
    return p

def visualizeFit(X, mu, sigma2):
    """Visualize the dataset and its estimated distribution.

    visualizeFit(X, p, mu, sigma2) This visualization shows you the
    probability density function of the Gaussian distribution. Each example
    has a location (x1, x2) that depends on its feature values.
    :param X:
    :param mu:
    :param sigma2:
    :return:
    """
    X1, X2 = np.meshgrid(np.arange(0, 35.5, 0.5), np.arange(0, 35.5, 0.5))
    Z = multivariateGaussian(np.column_stack([X1.reshape(-1), X2.reshape(-1)]), mu, sigma2)
    Z = Z.reshape(X1.shape)

    plt.plot(X[:, 0], X[:, 1], 'bx')
    # Do not plot if there are infinities
    if np.sum(np.isinf(Z)) == 0:
        plt.contour(X1, X2, Z, 10.0 ** np.arange(-20, 1, 3))

def selectThreshold(yval, pval):
    """Find the best threshold (epsilon) to use for selecting outliers

    [bestEpsilon bestF1] = selectThreshold(yval, pval) finds the best
    threshold to use for selecting outliers based on the results from a
    validation set (pval) and the ground truth (yval).
    :param yval:
    :param pval:
    :return:
    """
    bestEpsilon = np.nan
    bestF1 = 0

    stepsize = (max(pval) - min(pval)) / 1000
    for epsilon in np.arange(min(pval) + stepsize, max(pval) + stepsize, stepsize):
        predictions = (pval < epsilon).astype(int)
        precision = ((predictions == 1) & (yval == 1)).sum() / (predictions == 1).sum()
        recall = ((predictions == 1) & (yval == 1)).sum() / (yval == 1).sum()
        F1 = 2 * precision * recall / (precision + recall)

        if F1 > bestF1:
            bestF1 = F1
            bestEpsilon = epsilon

    return bestEpsilon, bestF1


"""
Problem 6

Find the difference between the sum of the squares of the first one hundred natural numbers and the square of the sum.
"""

import numpy as np

def sum_of_squares(range_start, range_end):
    return sum(i**2 for i in range(range_start, range_end))

def square_of_sum(range_start, range_end):
    return sum(range(range_start, range_end)) ** 2

def compute_difference(range_start, range_end):
    a = sum_of_squares(range_start, range_end)
    b = square_of_sum(range_start, range_end)

    return np.abs(a - b)

print(compute_difference(1, 101))


import numpy as np
import math

'''
Different combination methods using python
'''

# Using raw mathematics
def comb(n, r):
  num = denom_a = denom_b= 1

  # n!
  if n >= 1:
    for i in range (1,n+1):
      num=num*i

  if r < n:
    # r!
    for i in range (1,r+1):
      denom_a=denom_a*i

    # (n-r)!
    for i in range (1,n-r+1):
      denom_b=denom_b*i

  return num/(denom_a*denom_b)

# Using factorial
def comb_fact(n, r):
  return math.factorial(n)/(math.factorial(r)*math.factorial(n-r))

# Using Stirling Approximation
def comb_stir(n,r):
  def stirling(n):
      return math.sqrt(2*math.pi*n)*(n/math.e)**n
  return stirling(n)/(stirling(r)*stirling(n-r))

# Using list
def comb_list(n, r):
  num_list = []
  denom_a = 1
  denom_list = []

  if n >= 1:
    for i in range (1,n+1):
      num_list.append(i)

  if r < n:
    for i in range (1,r+1):
      denom_a=denom_a*i

    for i in range (1,n-r+1):
      denom_list.append(i)

  # or just use math.factorial
  # denom_a = math.factorial(r)

  new_num = [x for x in num_list if x not in denom_list]

  return np.prod(new_num)/denom_a

# Using list (shortened)
def comb_list_v2(n, r):
  num_list = []
  denom_a = 1
  denom_list = []

  if r < n:
    for i in range (1,r+1):
        denom_a=denom_a*i

  if n >= 1:
    for i in range (n-r+1,n+1):
      num_list.append(i)

  # or just use math.factorial
  # denom_a = math.factorial(r)

  return np.prod(num_list)/denom_a


import numpy as np
from numpy.core.function_base import linspace
import scipy
import scipy.stats
import matplotlib.pyplot as plt
shape = 2
scale = 3
rv_gamma = scipy.stats.gamma(shape, scale=scale)
rv_inv_gamma = scipy.stats.invgamma(shape, scale=scale)
l = 1000
sample_gamma = rv_gamma.rvs(size=[l])
sample_inv_gamma = rv_inv_gamma.rvs(size=[l])
axes = []
fig,ax = plt.subplots()
ax.hist(sample_gamma, bins="auto", density=True, color=[1,0.4,0.4,0.4], label="gamma distribution")
axes.append(ax)
for i in range(2):
    axes.append(ax.twinx())
axes[1].hist(sample_inv_gamma, bins="auto", density=True, color=[0.4,0.4,1,0.4], label="inv-gamma distribution")

x = np.linspace(1e-8,20)
def pdf_gamma(x,shape, scale):
    return 1/(scipy.special.gamma(shape)*shape**scale) * x**(shape-1) * np.exp(-x/scale)
def pdf_inv_gamma(x, shape, scale):
    return scale**shape /scipy.special.gamma(shape) * x**(-shape-1) * np.exp(-scale/x)
y_gamma = scipy.stats.gamma.pdf(x, shape, scale=scale)
y_gamma_m = pdf_gamma(x, shape, scale)
y_inv_gamma = pdf_inv_gamma(x,shape, scale)
axes[2].plot(x,y_gamma, color=[1,0.4,0.4,0.9])
axes[2].plot(x,y_gamma_m, color=[0.4,1,0.4,0.9])
axes[2].plot(x,y_inv_gamma, color=[0.4,0.4,1,0.9])
for ax in axes:
    ax.set_xlim(0,20)
    ax.set_ylim(0,1.5)
plt.savefig("./q3_explore_2.png")