X = [0, 20, 40, 60, 80, 100]
Y = [26.0, 48.6, 61.6, 71.2, 74.8, 75.2]

n = len(X)-1    # Degree of polynomial = number of points - 1

print("X =", X)
print("Y =", Y, end='\n\n')

xp = float(input("Find Y for X = "))

# For degree of polynomial 3, number of points n+1 = 4:
# L[1] = (x-x2)/(x1-x2) * (x-x3)/(x1-x3) * (x-x4)/(x1-x4)
# L[2] = (x-x1)/(x2-x1) * (x-x3)/(x2-x3) * (x-x4)/(x2-x4)
# L[3] = (x-x1)/(x3-x1) * (x-x2)/(x3-x2) * (x-x4)/(x3-x4)
# L[4] = (x-x1)/(x4-x1) * (x-x2)/(x4-x2) * (x-x3)/(x4-x3)

# L[i] *= (x-xj)/(xi-xj) where i, j = 1 to n+1 and j != i
# y    += Y[i]*L[i]      where i    = 1 to n+1
                         # List index 0 to n

# ~~~~~~~~~~~~~~~~~~~~~~~~ Method 1: Using for loop ~~~~~~~~~~~~~~~~~~~~~~~~

yp = 0                          # Initial summation value
for i in range(n+1):

    L = 1                       # Initial product value
    for j in range(n+1):
        if j == i: continue     # j == i gives ZeroDivisionError
        L *= (xp - X[j]) / (X[i] - X[j])

    yp += Y[i]*L

# ~~~~~~~~~~~~~~~~~~~ Method 2: Using numpy array, prod ~~~~~~~~~~~~~~~~~~~~

from numpy import array, prod

X = array(X, float)
Y = array(Y, float)

yp = 0
for Xi, Yi in zip(X, Y):
    yp += Yi * prod((xp - X[X != Xi]) / (Xi - X[X != Xi]))

print("The Y = %.1f"% yp)

# ~~~~~~~~~~~~~~~~~~~~~~~~~ Plotting the function ~~~~~~~~~~~~~~~~~~~~~~~~~~

from numpy import append, linspace
from matplotlib.pyplot import plot, xlabel, ylabel, show

xplot = linspace(X[0], X[-1])   # Divides from 1st to last element of X
yplot = array([], float)        # into 50 points

for xp1 in xplot:

    yp1 = 0
    for Xi, Yi in zip(X, Y):
        yp1 += Yi * prod((xp1 - X[X != Xi]) / (Xi - X[X != Xi]))

    yplot = append(yplot, yp1)

# pyplot.
plot(X, Y, 'ro', xplot, yplot, 'b-', xp, yp, 'bo')
xlabel('x')
ylabel('y')
show()

2. Interpolation and Curve Fitting/2. Lagrange's Interpolation Method.py


'''
Exercise 3
5.0/5.0 points (graded)

Write a function, stdDevOfLengths(L) that takes in a list of strings, L, and outputs the standard deviation of the lengths of the strings. Return float('NaN') if L is empty.

Recall that the standard deviation is computed by this equation:

∑t in X(t−μ)2N−−−−−−−−−−−−−√

where:

    μ is the mean of the elements in X.

    ∑t in X(t−μ)2 means the sum of the quantity (t−μ)2 for t in X.

    That is, for each element (that we name t) in the set X, we compute the quantity (t−μ)2. We then sum up all those computed quantities.

    N is the number of elements in X.

    Test case: If L = ['a', 'z', 'p'], stdDevOfLengths(L) should return 0.

    Test case: If L = ['apples', 'oranges', 'kiwis', 'pineapples'], stdDevOfLengths(L) should return 1.8708.

Note: If you want to use functions from the math library, be sure to import math. If you want to use numpy arrays, you should add the following lines at the beginning of your code for the grader:
import os
os.environ["OPENBLAS_NUM_THREADS"] = "1"
Then, do import numpy as np and use np.METHOD_NAME in your code.
'''
import numpy as np

def stdDevOfLengths(L):
    """
    L: a list of strings

    returns: float, the standard deviation of the lengths of the strings,
      or NaN if L is empty.
    """
    if L == [] or L is None:
        return np.nan
    else:
        mean = sum(len(string) for string in L) / len(L)
        std_dev = (sum((len(string) - mean) **
                       2 for string in L) / len(L)) ** 0.5
        return std_dev


# math module
import math

# we can see all the methods of math module.
print(dir(math))

# we can see that we have different important methods here.
# ['__doc__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh',
#  'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'comb', 'copysign', 'cos', 'cosh',
#  'degrees', 'dist', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor',
#  'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf',
# 'isnan', 'isqrt', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'perm',
#  'pi', 'pow', 'prod', 'radians', 'remainder', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc']

# calculate natural logarithm
print(math.log(10))

# calculate common logarithm
print(math.log10(100))

# calculate 2 based logarithm
print(math.log2(1024))

# calculate factorial of any number
print(math.factorial(9))

# calculate square root of any positive number
print(math.sqrt(100))

# calculate square root of any negative number.
# here the square root of any negative number is a complex number.
import cmath # we need to import cmath module which stands for complexmath
print(cmath.sqrt(-20))

# using floor method
# floor method rounds a number to its lower nearest integer.
print(math.floor(2.34))

# using ceil method
# ceil method rounds a number to its higher nearest integer.
print(math.ceil(2.34))

# print the value of pi
import math
print(math.pi)
# print the value of tau
print(math.tau)
# print the value of e
print(math.e)

# calculate the area of a circle.
def circle_area(radius):
    area=math.pi*(radius**2)
    return area
print(circle_area(10))

# using Euelers constant
print(math.e)

# convert degrees to radians
print(math.radians(180))

# convert radians to degrees
print(math.degrees(3))

# basic trigonometry
# using sin method
print(math.sin(math.pi/2))# here the arguement is given in radians.

# using cos method
print(math.cos(0))

# using tan method
print(math.tan(math.pi/4))

# using asin(arcsin) method
print(math.asin(1))# here the arguement is given in radians.
# we can convert it to degrees.
print(math.degrees(math.asin(1)))

# using acos(arccos) method
print(math.acos(1))

# using atan(arctan) method
print(math.atan(1))

# calculate gcd of numbers
print(math.gcd(120,90))

# we can use the gamma() function.
print(math.gamma(100))

# we can even find the remainder of float using the remainder() function.
print(math.remainder(100.123,3.1))

# we can even find the power of float using pow() function.
print(math.pow(2.33,4.573))
# we can do it in this way too.
print(2.33**4.573)

# we can find the hypotnuse by hypot() method.
print(math.hypot(4,3))
# it just take the co-ordinate and calculate the length of the line from origin to that co-ordinate.

# we have infinite() and isinf() function to determine if a value is finite or infinity.
print(math.isfinite(334))
print(math.isinf(77))
# we can create infinite value by inf attribute.
p=math.inf
print(math.isinf(p))

# isqurt() returns the integer part of the square root of the input.
print(math.isqrt(66))

# we can create nan values with nan attribute.
a=math.nan
# we can examine if it is a nan or not by isnan() method.
print(math.isnan(a))

# we can find the euclidian distance beetween two point in any dimention.
l=[0,10,20,30,5]
r=[0,10,24,3,89]
print(math.dist(l,r))

# using linear algebra we can calculate this manually,
import numpy as np
a1=np.array(l)
a2=np.array(r)

a3=a1-a2

res=0
for elements in a3:
    res += elements**2

print(np.sqrt(res))

# we can find the number of combination using comb() method.
n=10
r=4
print(math.comb(n,r))

# we can find the number of permutation using perm() method.
n=10
r=4
print(math.perm(n,r))

# copysign() returns the magnitute(value) of first arguement and sign(+-) of second arguement.
print(math.copysign(20.56,-56.7))

# we haave some more functions and constants in math module.
# we will use them later if needed.


# Question 01, Lab 04
# AB Satyaprakash - 180123062

# imports ----------------------------------------------------------------------------
from sympy.abc import x
from sympy import cos, exp, pi, evalf, simplify

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

def midpointRule(f, a, b):
    return ((b-a)*f.subs(x, (b-a)/2)).evalf()

def trapezoidalRule(f, a, b):
    return (((b-a)/2)*(f.subs(x, a)+f.subs(x, b))).evalf()

def simpsonRule(f, a, b):
    return (((b-a)/6)*(f.subs(x, a)+4*f.subs(x, (a+b)/2)+f.subs(x, b))).evalf()

# program body
# part (a) I = integrate cosx/(1+cos^2x) from 0 to π/2 -- exact value ≈ 0.623225
f = cos(x)/(1 + cos(x)**2)
a, b = 0, pi/2

print('To integrate {} from {} to {}'.format(simplify(f), a, b))
print('Evaluated value of integral using Midpoint rule is', midpointRule(f, a, b))
print('Evaluated value of integral using Trapezoidal rule is',
      trapezoidalRule(f, a, b))
print('Evaluated value of integral using Simpson rule is', simpsonRule(f, a, b))
print('Exact value ≈ 0.623225\n')

# part (b) I = integrate 1/(5+4cosx) from 0 to π -- exact value ≈ 1.047198
f = 1/(5 + 4*cos(x))
a, b = 0, pi

print('To integrate {} from {} to {}'.format(simplify(f), a, b))
print('Evaluated value of integral using Midpoint rule is', midpointRule(f, a, b))
print('Evaluated value of integral using Trapezoidal rule is',
      trapezoidalRule(f, a, b))
print('Evaluated value of integral using Simpson rule is', simpsonRule(f, a, b))
print('Exact value ≈ 1.047198\n')

# part (c) I = integrate exp(-x^2) from 0 to 1 -- exact value ≈ 0.746824
f = exp(-x**2)
a, b = 0, 1

print('To integrate {} from {} to {}'.format(simplify(f), a, b))
print('Evaluated value of integral using Midpoint rule is', midpointRule(f, a, b))
print('Evaluated value of integral using Trapezoidal rule is',
      trapezoidalRule(f, a, b))
print('Evaluated value of integral using Simpson rule is', simpsonRule(f, a, b))
print('Exact value ≈ 0.746824\n')

Semester 6/MA 322 (Scientific Computing Theory)/Labs/Lab 4/Code/q1.py


from numpy import exp, linspace, empty
f = lambda x: exp(x-2) - 3  # Analytical Solution

dy = lambda x, y: y+3   # Equation to be solved, y' = y+3
x = 2                   # Lower limit, [2
xn = 4                  # Upper limit, 4]
y = -2                  # Initial condition, y(2) = -2

h = 0.1                 # Width of each division, step size
n = int((xn-x)/h)       # Number of divisions of the domain

# Plot Arrays
xp = linspace(x, xn, n+1)   # Divides from x to xn into n+1 points
yp = empty(n+1, float)
yp[0] = y

print('x \t\ty(RK4) \t\ty(Analytical)')   # Header of Output
print('%f \t% f \t% f' % (x, y, f(x)))    # Initial x and y

for i in range(1, n+1):
    K1 = h * dy(x,y)
    K2 = h * dy(x + h/2, y + K1/2)
    K3 = h * dy(x + h/2, y + K2/2)
    K4 = h * dy(x + h, y + K3)

    y += 1/6*(K1 + 2*K2 + 2*K3 + K4)    # y(x+h) = y(x) + 1/6(K1+2K2+2K3+K4)
    yp[i] = y
    x += h                              # x for next step, x = x + h
    print('%f \t% f \t% f' % (x, y, f(x)))

# ~~~~~~~~~~~~~~~~~~~~~~~~~ Plotting the function ~~~~~~~~~~~~~~~~~~~~~~~~~~
import matplotlib.pyplot as plt

# pyplot.
plt.plot(xp, yp, 'ro', xp, f(xp))   # Default plot is continuous blue line
plt.xlabel('x')
plt.ylabel('y')
plt.legend(['RK4', 'Analytical'])
plt.show()

"""
Taylor's series can be written as
y(x+h) = y(x) + y'(x)h + y''(x)/2! h^2 + y'''(x)/3! h^3 + ...

Approximate solution of Euler's method is obtained by
trancating the series at the first derivative term
y(x+h) = y(x) + y'(x)h

Including the second, third, and forth derivatives

K1 = h y'(x,y)
K2 = h y'(x + h/2, y + K1/2)
K3 = h y'(x + h/2, y + K2/2)
K4 = h y'(x + h, y + K3)
y(x+h) = y(x) + 1/6 (K1 + 2 K2 + 2 K3 + K4)

The initial condition is the value of y(x) at initial domain x

Find the numerical solution of the following differential equation
over the domain [2,4]: y' = y+3,        y(2) = -2

Analytical Solution: y = e^(x-2) - 3

"""

6. Ordinary Differential Equations/3. b. Plotting Fourth Order Runge-Kutta (RK4) Method.py


from math import sqrt
from numpy.testing import assert_equal, assert_allclose

def real_quadratic_roots(a, b, c):
    """
    Find the real roots of the quadratic equation a x^2 + b x + c = 0, if they exist.

    Parameters
    ----------

    a : float
        Coefficient of x^2
    b : float
        Coefficient of x^1
    c : float
        Coefficient of x^0

    Returns
    -------

    roots : tuple or float or None
        The root(s) (two if a genuine quadratic, one if linear, None otherwise)

    Raises
    ------

    NotImplementedError
        If the equation has trivial a and b coefficients, so isn't solvable.
    """

    discriminant = b**2 - 4.0*a*c
    if discriminant < 0.0:
        return None

    if a == 0:
        if b == 0:
            raise NotImplementedError("Cannot solve quadratic with both a"
                                      " and b coefficients equal to 0.")
        else:
            return -c / b

    x_plus = (-b + sqrt(discriminant)) / (2.0*a)
    x_minus = (-b - sqrt(discriminant)) / (2.0*a)

    return x_plus, x_minus

def test_no_roots():
    """
    Test that the roots of x^2 + 1 = 0 are not real.
    """

    roots = None
    assert_equal(real_quadratic_roots(1, 0, 1), roots,
                 err_msg="Testing x^2+1=0; no real roots.")

def test_zero_roots():
    """
    Test that the roots of x^2 = 0 are both zero.
    """

    roots = (0, 0)
    assert_equal(real_quadratic_roots(1, 0, 0), roots,
                 err_msg="Testing x^2=0; should both be zero.")

def test_real_distinct():
    """
    Test that the roots of x^2 - 1 = 0 are \pm 1.
    """

    roots = (1.0, -1.0)
    assert_equal(real_quadratic_roots(1, 0, -1), roots,
                 err_msg="Testing x^2-1=0; roots should be 1 and -1.")

def test_real_distinct_irrational():
    """
    Test that the roots of x^2 - 2 x + (1 - 10**(-10)) = 0 are 1 \pm 1e-5.
    """

    roots = (1 + 1e-5, 1 - 1e-5)
    assert_allclose(real_quadratic_roots(1, -2.0, 1.0 - 1e-10), roots,
                 err_msg="Testing x^2-2x+(1-1e-10)=0; roots should be 1 +- 1e-5.")

def test_real_linear_degeneracy():
    """
    Test that the root of x + 1 = 0 is -1.
    """

    root = -1.0
    assert_equal(real_quadratic_roots(0, 1, 1), root,
                 err_msg="Testing x+1=0; root should be -1.")


import numpy as np

# Arithmetic operators on arrays apply element wise. A new array is created and filled with the result.
print("<---- Arithmatic Operation --->")
a = np.array([12,45,85,96])
b = np.arange(5,9)
print("Subtraction : ",a-b)
print("Power of 2 of b array : ",b**2)
print("Sin of a : ",10*np.sin(a))
print("Less than operator : ",a<79)

# Unlike in many matrix languages, the product operator * operates element wise in NumPy arrays.
# The matrix product can be performed using the @ operator (in python >=3.5) or the dot function
# or method
print("<---- Product of array ---->")
a = np.array([(1,2),(3,4)])
b = np.array([[5,6],[7,8]])
print("Element wise product : ",a*b)
print("Matrix product : ",a@b)
print("Another matrix product : ",a.dot(b))

# Some operations, such as += and *=, act in place to modify an existing array rather than
# create a new one.
print("<---- Assignment operator --->")
a = np.ones(shape=(2,3),dtype=int)
b = np.random.rand(2,3)
a *= 3
print("Multiply by 3 to each element of Array a : ",a)
# a += b this is not possible here because a in type of int and b is float because randome gives
# float default
b += a
print("Adding each element of a with b : ",b)

# When operating with arrays of different types, the type of the resulting array corresponds to
# the more general or precise one (a behavior known as upcasting).
print("<---- Upcasting of array ---->")
a = np.ones(3,dtype=np.int32)
b = np.linspace(0,np.pi,3)
print("dtype of b : ",b.dtype)
c = a + b
print("Addition of a and b : ",c)
print("dtype of c : ",c.dtype)
d = np.exp(c*1j)
print("exp of c : ",d)
print("dtype of d : ",d.dtype)

# Many unary operations, such as computing the sum of all the elements in the array, are
# implemented as methods of the ndarray class.
print("<---- Common ndarray operations ---->")
a = np.random.random((3,2))
print("Sum : ",a.sum())
print("Min : ",a.min())
print("Max : ",a.max())

# By default, these operations apply to the array as though it were a list of numbers,
# regardless of its shape. However, by specifying the axis parameter you can apply an operation
# along the specified axis of an array:
print("<---- Common ndarray operations using axis ---->")
b = np.arange(12).reshape(3,4)
print("b Array : ",b)
print("Sum of each column : ",b.sum(axis=0))
print("Sum of each row : ",b.sum(axis=1))
print("Cumulative sum along each row : ",b.cumsum(axis=1))


"""
### Merge sort
Complexity:
- $6n$ operations per level
- $\log_2(n)$ recursive calls
- Total in time : $6n \log_2(n) + 6n = O(n\log(n))$
- In memory: $O(n)$ as need not in place

Inversions algorithm
Complexity:
same as merge sort, $O(n\log(n))$ in time and $O(n)$ in memory
"""

import numpy as np

def merge_sort(A):
    def merge_sort_(A, l, r):
        if l < r:
            m = (l + r - 1) // 2
            merge_sort_(A, l, m)
            merge_sort_(A, m + 1, r)
            combine(A, l, m, r)

    def combine(A, l, m, r):
        n_l = m - l + 1
        n_r = r - m
        L = A[l : m + 1]
        R = A[m + 1 : r + 1]
        i, j, k = 0, 0, l
        while i < n_l and j < n_r:
            if L[i] <= R[j]:
                A[k] = L[i]
                i += 1
            else:
                A[k] = R[j]
                j += 1
            k += 1
        while i < n_l:
            A[k] = L[i]
            k += 1
            i += 1
        while j < n_r:
            A[k] = R[j]
            k += 1
            j += 1

    return merge_sort_(A, 0, len(A) - 1)

def count_inversions(A):
    def sort_and_count_inversions(A, l, r):
        if r <= l:
            return 0
        else:
            m = (l + r - 1) // 2
            x = sort_and_count_inversions(A, l, m)
            y = sort_and_count_inversions(A, m + 1, r)
            z = combine_and_count_inv_split(A, l, m, r)
        return x + y + z

    def combine_and_count_inv_split(A, l, m, r):
        n_l = m - l + 1
        n_r = r - m
        L = A[l : m + 1]
        R = A[m + 1 : r + 1]
        i, j, k = 0, 0, l
        count = 0
        while i < n_l and j < n_r:
            if L[i] <= R[j]:
                A[k] = L[i]
                i += 1
            else:
                A[k] = R[j]
                count += n_l - i
                j += 1
            k += 1
        while i < n_l:
            A[k] = L[i]
            k += 1
            i += 1
        while j < n_r:
            A[k] = R[j]
            k += 1
            j += 1
        return count

    return sort_and_count_inversions(A, 0, len(A) - 1)

if __name__ == "__main__":
    A = np.loadtxt("./Course_1/countinv.txt").tolist()
    # A = [1, 23, 5, 2, 4, 6]
    count_inversions(A)


#!/usr/bin/env python
# coding: utf-8
## Running a Multivariate Regression in Python
# *Suggested Answers follow (usually there are multiple ways to solve a problem in Python).*
# Let’s continue working on the file we used when we worked on univariate regressions.
# *****
# Run a multivariate regression with 5 independent variables – from Test 1 to Test 5.
import numpy as np
import pandas as pd
from scipy import stats
import statsmodels.api as sm
import matplotlib.pyplot as plt
data = pd.read_excel('Section-13_IQ_data.xlsx')
data.head()
# ### Multivariate Regression:
# Independent Variables: *Test 1, Test 2, Test 3, Test 4, Test 5*
# In[4]:
X = data[['Test 1', 'Test 2', 'Test 3', 'Test 4', 'Test 5']]
X = data.iloc[:,[1,2,3,4,5]]
Y = data.iloc[:,0]
# In[5]:
X1 = sm.add_constant(X)
reg = sm.OLS(Y, X1).fit()
reg.summary()
# The p-value of the variable Test 1 in the univariate regression looked very promising. Is it still low? If not – what do you think would be the reason for the change?
# Try regressing Test 1 and Test 2 on the IQ score first. Then, regress Test 1, 2, and 3 on IQ, and finally, regress Test 1, 2, 3, and 4 on IQ. What is the Test 1 p-value in these regressions?
# Independent Variables: *Test 1, Test 2*
# In[6]:
X = data[['Test 1', 'Test 2']]
X = data.iloc[:,[1,2]]
Y = data.iloc[:,0]
# In[7]:
X1 = sm.add_constant(X)
reg = sm.OLS(Y, X1).fit()
reg.summary()
# Independent Variables: *Test 1, Test 2, Test 3*
# In[8]:
X = data[['Test 1', 'Test 2', 'Test 3']]
X = data.iloc[:,[1,2,3]]
Y = data.iloc[:,0]
# In[9]:
X1 = sm.add_constant(X)
reg = sm.OLS(Y, X1).fit()
reg.summary()
# Independent Variables: *Test 1, Test 2, Test 3, Test 4*
# In[10]:
X = data[['Test 1', 'Test 2', 'Test 3', 'Test 4']]
X = data.iloc[:,[1,2,3,4]]
Y = data.iloc[:,0]
# In[11]:
X1 = sm.add_constant(X)
reg = sm.OLS(Y, X1).fit()
reg.summary()
# It seems it increases a lot when we add the result from Test 4.
# ****
# Run a regression with only Test 1 and Test 4 as independent variables. How would you interpret the p-values in this case?
# Independent Variables: *Test 1, Test 4*
# In[12]:
X = data[['Test 1', 'Test 4']]
X = data.iloc[:,[1,4]]
Y = data.iloc[:,0]
# In[13]:
X1 = sm.add_constant(X)
reg = sm.OLS(Y, X1).fit()
reg.summary()
# Independent Variables: *Test 4*
# In[14]:
X = data[['Test 4']]
X = data.iloc[:,4]
Y = data.iloc[:,0]
# In[15]:
X1 = sm.add_constant(X)
reg = sm.OLS(Y, X1).fit()
reg.summary()
# ***Suggested Answer:***
# *Test 1 and Test 4 are correlated, and they contribute to the preparation of the IQ test in a similar way.*

import numpy,from scipy,import statsmodels


import math
import numpy as np

def distance_calculator(x1, y1, x2, y2):
    """
    function to return Euclidean distance between two coordinates (x1,y1) and (x2,y2)
    """
    return math.sqrt((x2 - x1)**2 + (y2-y1)**2)

def regular_polygon(side_number, side_length, dir_counter_clock=True):
    """
    function to return coordinates of vertices on the regular polygon
    """

    # unit radius
    radius = 1

    # using cosine 2 rule
    init_side_length = math.sqrt(2*(radius**2) - 2*(radius**2)*math.cos(2*math.pi/side_number))

    # we want the the polygon's side length as input target; thus multiply this ratio
    ratio = side_length/init_side_length

    # find coordinate list
    coordinate_list = []
    for i in range(side_number):
        coordinate_list.append((radius * ratio * math.cos(2*math.pi * i/side_number), radius * ratio * math.sin(2*math.pi * i/side_number)))

    # append the starting point at the end to make the robot return home
    coordinate_list.append(coordinate_list[0])

    # clock-wise rotation coordinates
    if dir_counter_clock is False:
        coordinate_list = coordinate_list[::-1] # reverse the cooridnate list

    print("======================================================")
    print("side number: {}".format(side_number))
    print("side length: {}".format(side_length))
    print("counter-clock direction?: {}".format(dir_counter_clock))
    print("double check side by coordinate: {}".format(distance_calculator(coordinate_list[0][0],
                                                coordinate_list[0][1], coordinate_list[1][0], coordinate_list[1][1])))

    print("coordinate list: {}".format(coordinate_list))
    print("======================================================")

    return coordinate_list

def check_vector_angle(vec1, vec2):
    """
    check angle between two vectors by using vec 1 as base.
    - arg: vec1, vec2 format np.array([x,y]) 1x2
    - return: angle between the vectors abs 0 to pi (+: if coincide with check vector, -: if not)
    """

    # inner dot calculation
    inner_dot = np.dot(vec1, vec2)

    # vector length calculation
    vec1_len = np.linalg.norm(vec1)
    vec2_len = np.linalg.norm(vec2)

    # between angle calculation
    between_angle = math.acos(round(np.asscalar(inner_dot / (vec1_len * vec2_len)),6))

    # check vector (assuming "between angle" is correct, we get rotation vector of vec1 based on that angle)
    # length normalization and magnitude reflection
    check_vector = [vec1[0] * math.cos(between_angle) - vec1[1] * math.sin(between_angle),
                        vec1[0] * math.sin(between_angle) + vec1[1] * math.cos(between_angle)]
    check_vector_normalized = [check_vector[i] / vec1_len for i in range(len(check_vector))]
    vec2_normalized = [vec2[i] / vec2_len for i in range(len(check_vector))]

    # check if the vector is the same as what we want
    # note that 0.1 is just a threshhold for comparison
    if abs(check_vector_normalized[0] - vec2_normalized[0]) < 0.1 and abs(check_vector_normalized[1] - vec2_normalized[1]) < 0.1:
        return between_angle
    return -between_angle


import numpy as np
import matplotlib.pyplot as mp

# Equiation : f(U) + H(U) * V = 0
# <=> H(U) * V = -f(U)

# Transforms an array of functions to a function returning an array of the values of each function
def function_array(f):
	return lambda U: np.vectorize(lambda fn: fn(U))(f)

# Produces a value indicating how x is near to a root of f. Returns a value close to 0 if x is near a root and a value far from 0 if x is far from any root.
def phi(f, x):
	fx = f(x)
	return np.matmul(fx.T, fx)

# Finds a root of f where J is the Jacobian matrix of j.
# Parameters:
# - U0 the starting point of the algorithm
# - N the maximum number if iterations
# - epsilon the desired accuracy
# - backtracking wether backtraking should be used
# - track_phi wether the value of phi(Un) should be tracked and returned in an array
# Returns:
# - A root of f if track_phi is False
# - A root of f and the array of phi(Un) if track_phi is True
def Newton_Raphson(f, J, U0, N, epsilon, backtracking = True, track_phi = False):

	Un = U0
	previousPhi = phi(f, U0)
	if track_phi:
		phiArray = [previousPhi]

	for i in range(N):
		fUn = f(Un)
		if np.linalg.norm(fUn) < epsilon:
			break;

		V = np.linalg.lstsq(J(Un), -fUn, rcond=-1)[0]

		# Backtracking
		step = 1.0
		newUn = Un + V
		newPhi = phi(f, newUn)
		if backtracking:
			while newPhi >= previousPhi:
				step *= 0.9;
				newUn = Un + step * V
				newPhi = phi(f, newUn)

		Un = newUn
		previousPhi = newPhi
		if track_phi:
			phiArray.append(newPhi)

	if track_phi:
		return (Un, phiArray)
	else:
		return Un

# Tests the Newton_Raphson algorithm with simple cases
def test_Newton_Raphson():
	# f1(x) = x² - 2x + 1
	# f1'(x) = 2x - 2
	f1 = function_array([lambda x: x**2 - 2*x + 1])
	J1 = function_array([[lambda x: 2*x - 2]])
	expected = 1
	root1 = Newton_Raphson(f1, J1, np.array([0]), 40, 10e-6)
	print("f(x) = x² - 2x + 1")
	print("Root:", root1)
	print("Relative error:", np.abs(root1 - expected))

	# f2(x, y) = (x² + y, x + 3y + 4)
	# df2/dx(x, y) = (2*x, 1)
	# df2/dy(x, y) = (1, 3)
	f2 = function_array([lambda z: z[0]**2 + z[1], lambda z: z[0] + 3*z[1] + 4])
	J2 = function_array([[lambda z: 2*z[0], lambda z: 1],
						 [lambda z: 1, lambda z: 3]])
	root2 = Newton_Raphson(f2, J2, np.array([1, 4]), 40, 1e-10)
	print("f(x, y) = (x² + y, x + 3y + 4)")
	print("Root:",root2)
	print("f(root) =", f2(root2))

# Tests the effets of backtracking on the convergence speed
def test_backtracking():
	# f(x) = x^3 - 2x^2 + 1
	# f'(x) = 3x^2 - 4x
	f = function_array([lambda x: x**3 - 2*x**2 + 1])
	J = function_array([[lambda x: 3*(x**2) - 4*x]])
	root0, phiArray0 = Newton_Raphson(f, J, np.array([0.01]), 40, 10e-10, False, True)
	root1, phiArray1 = Newton_Raphson(f, J, np.array([0.01]), 40, 10e-10, True, True)

	mp.plot(range(len(phiArray0)), phiArray0, label = "Without Backtracking", linewidth = 1.0)
	mp.plot(range(len(phiArray1)), phiArray1, label = "With Backtracking", linewidth = 1.0)
	mp.yscale('log')
	mp.legend()
	mp.title("The value of $phi(U_n)$ in each iteration, with $f(x) = x^3 - 2x^2 + 1$, $U_0 = 0.01$ and $\\epsilon = 10^{-10}$")
	mp.xlabel("n, the number of iterations")
	mp.ylabel("$phi(U_n)$")
	mp.show()

if __name__ == "__main__":
	test_Newton_Raphson()
	test_backtracking()


import numpy as np
from math import sqrt

# Fibonacci version 1 in exponential time.
# The running time of fib1(n) is proportional to 2^(0.69n) ~ (1.6)^n, so it takes 1.6 times longer to compute Fn+1 than Fn.
def fib1(n):
    if n == 0: return 0
    if n == 1: return 1
    return fib1(n-1) + fib1(n-2)

# Fibonacci version 2 in linear time (Polynomial)
# Why is fib1(n) so slow? because many computations are repeated during the recursion. A more sensible scheme would store the intermediate results, the value of F0.....Fn-1 to an array as soon as they are know so that they don't have to be recomputed during each recursion call.
def fib2(n):
    if n == 0: return 0
    f = np.zeros(n+1, dtype=np.int) # Creates an array holds F0 to Fn
    f[0] = 0
    f[1] = 1
    for i in range(2, n+1): # Loops from f2 to fn (range is exclusive on the 2nd argument)
        f[i] = f[i-1] + f[i-2]
    return f[n]

# Fibonacci version 3 in constant time.
# Of course, the fastest is using Binet's Fibonacci Number Formula: http://mathworld.wolfram.com/BinetsFibonacciNumberFormula.html
def fib3(n):
    return ((1+sqrt(5))**n-(1-sqrt(5))**n)/(2**n*sqrt(5))

# Tests
print "fib1(10) = " + str(fib1(10))
print "fib2(10) = " + str(fib2(10))
print "fib3(10) = " + str(fib3(10))


import numpy as np
import scipy.integrate as spi
import matplotlib.pyplot as plt

#t is the independent variable
P = 3. #period value
BT=-6. #initian value of t (begin time)
ET=6. #final value of t (end time)
FS=1000 #number of discrete values of t between BT and ET

#the periodic real-valued function f(t) with period equal to P
f = lambda t: ((t % P) - (P / 2.)) ** 3

#all discrete values of t in the interval from BT and ET
t_range = np.linspace(BT, ET, FS)
y_true = f(t_range) #the true f(t)

#function that computes the real fourier couples of coefficients (a0, 0), (a1, b1)...(aN, bN)
def compute_real_fourier_coeffs(func, N):
    result = []
    for n in range(N+1):
        an = (2./P) * spi.quad(lambda t: func(t) * np.cos(2 * np.pi * n * t / P), 0, P)[0]
        bn = (2./P) * spi.quad(lambda t: func(t) * np.sin(2 * np.pi * n * t / P), 0, P)[0]
        result.append((an, bn))
    return np.array(result)

#function that computes the real form Fourier series using an and bn coefficients
def fit_func_by_fourier_series_with_real_coeffs(t, AB):
    result = 0.
    A = AB[:,0]
    B = AB[:,1]
    for n in range(0, len(AB)):
        if n > 0:
            result +=  A[n] * np.cos(2. * np.pi * n * t / P) + B[n] * np.sin(2. * np.pi * n * t / P)
        else:
            result +=  A[0]/2.
    return result

maxN=8
COLs = 2 #cols of plt
ROWs = 1 + (maxN-1) // COLs #rows of plt
plt.rcParams['font.size'] = 8
fig, axs = plt.subplots(ROWs, COLs)
fig.tight_layout(rect=[0, 0, 1, 0.95], pad=3.0)
fig.suptitle('f(t) = ((t % P) - (P / 2.)) ** 3 where P=' + str(P))

#plot, in the range from BT to ET, the true f(t) in blue and the approximation in red
for N in range(1, maxN + 1):
    AB = compute_real_fourier_coeffs(f, N)
    #AB contains the list of couples of (an, bn) coefficients for n in 1..N interval.

    y_approx = fit_func_by_fourier_series_with_real_coeffs(t_range, AB)
    #y_approx contains the discrete values of approximation obtained by the Fourier series

    row = (N-1) // COLs
    col = (N-1) % COLs
    axs[row, col].set_title('case N=' + str(N))
    axs[row, col].scatter(t_range, y_true, color='blue', s=1, marker='.')
    axs[row, col].scatter(t_range, y_approx, color='red', s=2, marker='.')
plt.show()


# Importing numpy library
import numpy as np

# Arrays to demonstrate unary ufuncs
randomint_arr = np.random.randint(-20,20,size = (2,3,4))
randomfloat_arr = np.random.randn(2,5,4)

# Unary Ufuncs
"""
>> ufuncs operate element by element on whole arrays.
>> ufuncs are highspeed because they are written in C.
Most common unary ufuncs include :
round, rint, fix, abs, absolute, fabs, sqrt, square, exp, exp2, log, log10, log2,
logp, sign, ceil, floor, modf, isnan, isinfinte, isinf, cos, cosh,
arccos, sin, sinh, arcsin, tan, tanh, arctan,logical_not
"""

# Round an array to the given number of decimals.
print(f'\n Round to 2 decimals = \n{np.round(randomfloat_arr,2)}')

# Round elements of the array to the nearest integer.
print(f'\n Round to nearest integer = \n{np.rint(randomfloat_arr)}')

# Round to nearest integer towards zero.
print(f'\n Round to nearest integer towards zero = \n{np.fix(randomfloat_arr,2)}')

# Computes the absolute value of a integer array; cannot handle complex values like a+ib
print(f'\nAbsolute value = \n{np.abs(randomint_arr)}')

# Calculates the absolute value for complex arrays too; absolute =  sqrt(a^2+b^2)
print(f'\nAbsolute value for complex numbers = \n{np.absolute(4+3j)}')

# Computes the absolute value of a floating point array
print(f'\nAbsolute value for floating point= \n{np.fabs(randomfloat_arr)}')

# Return the fractional and integral parts of an array, element-wise.
print(f'\n Fractional and integral parts =\n {np.modf(randomfloat_arr)}')

# Computes the Square root of the elements; sqrt of negative number gives nan
print(f'\nSquare root = \n{np.sqrt(np.abs((randomint_arr)))}')

# Computes the Square of the elements
print(f'\nSquare = \n{np.square(randomint_arr)}')

# Calculate the exponential of all elements in the input array.
print(f'\nExponential = \n{np.exp(randomint_arr)}')

# Calculate 2**p for all p in the input array.
print(f'\n 2**p = \n{np.exp2(np.array([1,2,3,4]))}')

# Return the natural logarithm of the input array, element-wise.
print(f'\nlog = \n{np.log(np.abs(randomint_arr))}')

# Return the base 10 logarithm of the input array, element-wise.
print(f'\nlog10 =\n {np.log10(np.abs(randomint_arr))}')

# Returns the Base-2 logarithm of x.
print(f'\nlog2=\n {np.log2(np.abs(randomint_arr))}')

# Returns the natural logarithm of one plus the input array, element-wise.
print(f'\nlog1p = \n{np.log1p(np.abs(randomint_arr))}')

# Returns an element-wise indication of the sign of a number.
print(f'\nSign of array elements =\n {np.sign(randomint_arr)}')

# Return the ceiling of the input, element-wise.
print(f'\n Ceiling of elements in array =\n {np.ceil(randomfloat_arr)}')

# Return the floor of the input, element-wise.
print(f'\n Floor of elements in array =\n {np.floor(randomfloat_arr)}')

# Returns the Trigonometric sine, element-wise.
print(f'\nsin(array) =\n {np.sin(randomint_arr)}')

# Returns the Hyperbolic sine, element-wise.
print(f'\nsinh(array) =\n {np.sinh(randomint_arr)}')

# Returns the Inverse sine, element-wise;
print(f'\narcsin(array) =\n {np.arcsin(np.array([-1, 1, 0]))}')

# Returns the Trigonometric cosine, element-wise.
print(f'\ncos(array) =\n {np.cos(randomint_arr)}')

# Returns the Hyperbolic cosine, element-wise.
print(f'\ncosh(array) =\n {np.cosh(randomint_arr)}')

# Returns the Inverse cosine, element-wise;
print(f'\narccos(array) =\n {np.arccos(np.array([-1, 1, 0]))}')

# Returns the Trigonometric sine, element-wise.
print(f'\ntan(array) =\n {np.tan(randomint_arr)}')

# Returns the Hyperbolic tan, element-wise.
print(f'\ntanh(array) =\n {np.tanh(randomint_arr)}')

# Returns the Inverse tan, element-wise;
print(f'\narctan(array) =\n {np.arctan(np.array([-1, 1, 0]))}')

# Creating an array with nan and inf
arr_naninf = np.array([[np.inf,np.nan,np.NaN],
                       [np.NAN,33,0],
                       [5/np.inf,np.inf*np.nan,np.inf/np.inf]])

print(f'\nOriginal array = \n{arr_naninf}')

# Test element-wise for NaN and return result as a boolean array.
print(f'\n Boolean array checking for nan =\n {np.isnan(arr_naninf)}')

# Test element-wise for inf and return result as a boolean array.
print(f'\n Boolean array checking for inf =\n {np.isinf(arr_naninf)}')

# Test element-wise for finiteness (not infinity or not Not a Number).
print(f'\n Boolean array checking for finiteness =\n {np.isfinite(arr_naninf)}')

# Some concepts regarding NaN and inf

# nan is never equal to nan; result of nan==nan is always false
print("nan==nan ? {}".format(np.nan==np.nan))

# inf is equal to inf;
print("inf==inf ? {}".format(np.inf==np.inf))

# np.any on NaN, positive infinity and negative infinity evaluate to True
print(f"np.any(np.NaN) is {np.any(np.NaN)}")
print(f"np.any(+np.inf) is {np.any(+np.inf)}")
print(f"np.any(-np.inf) is {np.any(-np.inf)}")

# np.all is True if all elements over the axis chosen in True
# np.all performs a logical AND between elements on the axis chosen
print(f"\nnp.all(arr_naninf,axis=0) = \n{np.all(arr_naninf,axis=0)}")
print(f"\narr_naninf.all(axis=1) = \n{arr_naninf.all(axis=1)}")

# Binary universal functions
"""
Since these operators always result in a boolean array, we can use
these ufuncs to do element-wise comparisons over arrays.
We can use the  operators or their equivalent ufunc for
accomplishing the task.
For instance, Operator "+" is same as "np.add".
"""
# Arrays to demonstrate binary ufuncs that utilize mathematical operations
arr_x = np.round(np.random.randn(2,3,4),2)
arr_y = np.random.randint(10,size=(2,3,4))

# Mathematical Operators
# Add arguments element-wise.
print(f'\nSum = \n{np.add(arr_x,arr_y)}')
print(f'\nSum = \n{arr_x+arr_y}')

# Subtracts arguments element-wise.
print(f'\nDifference = \n{np.subtract(arr_x,arr_y)}')
print(f'\nDifference = \n{arr_x-arr_y}')

# Multiply arguments element-wise.
print(f'\nProduct = \n{np.multiply(arr_x,arr_y)}')
print(f'\nProduct = \n{arr_x*arr_y}')

# Returns a true division of the inputs, element-wise.
print(f'\n Result of True Divison = \n{np.divide(arr_x,arr_y)}')
print(f'\n Result of True Divison = \n{np.true_divide(arr_x,arr_y)}')
print(f'\n Result of True Divison = \n{arr_x/arr_y}')

# Return element-wise remainder of division.
print(f'\n Remainder of True Divison = \n{np.mod(arr_x,arr_y)}')
print(f'\n Remainder of True Divison = \n{np.remainder(arr_x,arr_y)}')
print(f'\n Remainder of True Divison = \n{arr_x%arr_y}')

# Return element-wise quotient and remainder simultaneously.
# print(f'\n Quotient, Remainder = \n{np.divmod(arr_x,arr_y)}')

# Return the largest integer smaller or equal to the division of the inputs.
print(f'\n Result of Floor Divison = \n{arr_x//arr_y}')
print(f'\n Result of Floor Divison = \n{np.floor_divide(arr_x,arr_y)}')

# First array elements raised to powers from second array, element-wise.
print(f'\n arr_y to the power of arr_x = \n{np.power(arr_y,arr_x)}')
print(f'\n arr_y to the power of arr_x = \n{arr_y**arr_x}')

# Element-wise maximum of array elements.
print(f'\nmaximum of arrays  = \n{np.maximum(arr_y,arr_x)}')

# Element-wise minimum of array elements.
print(f'\nminimum of arrays  = \n{np.minimum(arr_y,arr_x)}')

# Change the sign of x1 to that of x2, element-wise.
print(f'\n Changes sign of array 1 to that of 2  = \n{np.copysign(arr_x,arr_y)}')

# Comparison Operators
"""
Comparison operators discussed here include :
equal, less_equal, less, greater_equal, greater
array_equiv, array_equal
"""
# Arrays to demonstrate binary ufuncs that utilize comparison operations
arr_x1 = np.arange(10)
arr_x2 = np.array([0,1,5,3,2,5,6,8,8,9])
arr_x3 = np.tile(arr_x1,(3,4,1))

# Return (x1 == x2) element-wise; x1 and x2 need not have same datatype
# x1 and x2 are arrays
print(f'\n Boolean array checking for equality  = \n{np.equal(arr_x1,arr_x2)}')
print(f'\n Boolean array checking for equality  = \n{arr_x1==arr_x2}')
# x1 is of "int" type and x2 is an array
print(f'\n Boolean array checking for equality  = \n{8==arr_x2}')
print(f'\n Boolean array checking for equality  = \n{np.equal(8,arr_x2)}')

# Return the truth value of (x1 =< x2) element-wise.
# x1 and x2 are arrays
print(f'\n Boolean array checking if 1st array =< 2nd array  = \n{np.less_equal(arr_x1,arr_x2)}')
print(f'\n Boolean array checking if 1st array =< 2nd array  = \n{arr_x1<=arr_x2}')
# x1 is an array and x2 is of "int" type
print(f'\n Boolean array checking if 1st array =< 2nd element  = \n{np.less_equal(arr_x2,5)}')
print(f'\n Boolean array checking if 1st array =< 2nd element  = \n{arr_x2<=5}')

# Return the truth value of (x1 < x2) element-wise.
# x1 and x2 are arrays
print(f'\n Boolean array checking if 1st array < 2nd array  = \n{np.less(arr_x1,arr_x2)}')
print(f'\n Boolean array checking if 1st array < 2nd array  = \n{arr_x1<arr_x2}')
# x1 is an array and x2 is of "int" type
print(f'\n Boolean array checking if 1st array < 2nd element  = \n{np.less(arr_x2,5)}')
print(f'\n Boolean array checking if 1st array < 2nd element  = \n{arr_x2<5}')

# Return the truth value of (x1 >= x2) element-wise.
# x1 and x2 are arrays
print(f'\n Boolean array checking if 1st array >= 2nd array  = \n{np.greater_equal(arr_x1,arr_x2)}')
print(f'\n Boolean array checking if 1st array >= 2nd array  = \n{arr_x1>=arr_x2}')
# x1 is an array and x2 is of "int" type
print(f'\n Boolean array checking if 1st array >= 2nd element  = \n{np.greater_equal(arr_x2,5)}')
print(f'\n Boolean array checking if 1st array >= 2nd element  = \n{arr_x2>=5}')

# Return the truth value of (x1 > x2) element-wise.
# x1 and x2 are arrays
print(f'\n Boolean array checking if 1st array > 2nd array  = \n{np.greater(arr_x1,arr_x2)}')
print(f'\n Boolean array checking if 1st array > 2nd array  = \n{arr_x1>arr_x2}')
# x1 is an array and x2 is of "int" type
print(f'\n Boolean array checking if 1st array > 2nd element  = \n{np.greater(arr_x2,5)}')
print(f'\n Boolean array checking if 1st array > 2nd element  = \n{arr_x2>5}')

# Returns True if input arrays are shape consistent and all elements equal.
print(f'\n Consistency in Shape and elements :{np.array_equiv(arr_x1,arr_x2)}')
# Returns True even if one of the arrays are broadcastable
print(f'\n Consistency in Shape and elements :{np.array_equiv(arr_x1,arr_x3)}')

# True if two arrays have the same shape and elements, False otherwise.
print(f'\n Equality in Shape and elements :{np.array_equal(arr_x1,arr_x2)}')
# Returns False even if one of them is broadcastable
print(f'\n Equality in Shape and elements :{np.array_equal(arr_x1,arr_x3)}')

# Logical Operands - Element-wise
# Compute the truth value of x1 XOR x2, element-wise; True only when one of the conditions is True
print(f'\n Boolean array checking for arr_x2 is either 5 or 8 : {np.logical_xor(arr_x2==5,arr_x2==8)}')

# Compute the truth value of x1 AND x2 element-wise; both conditions should hold
print(f'\n Boolean array checking for 1<= arr_x1 <=5 : \n{np.logical_and(arr_x1>=1,arr_x2<=5)}')

# Compute the truth value of x1 OR x2 element-wise; atleast one of the conditions should hold
print(f'\n Boolean array checking for elements in arr_x2>5 or arr_x2==0 : \n{np.logical_or(arr_x2==0,arr_x2>5)}')

# Compute the truth value of x1 NOT x2 element-wise.
print(f'\n Boolean array returning NOT(array): \n{np.logical_not(arr_x2==0)}')

# ufunc special methods
"""
All binary ufuncs support four methods for performing specialized functions.
>> reduce
>> accumulate
>> reduceat
>> outer
"""

# Array creation
arr_multidim = np.random.randint(9,size=(2,4,2,6))

# Reduces array’s dimension by one, by applying ufunc along one axis.
np.add.reduce(arr_multidim,axis=2,initial=10)
# Keyword "initial" lets you initialize the

# Accumulate the result of applying the operator to all elements.
np.add.accumulate(arr_multidim,axis=2)

# Performs a (local) reduce with specified slices over a single axis;
"""
np.ufunc.reduceat : Reduces contiguous slices of data to produce aggregated array
Format            : np.ufunc.reduceat(array,indices,axis,dtype,none)

For example : For i in range(length(indices)),reductions are performed on the contiguous slices of data
i < i+1 ; slice is i:(i+1) where (i+1) is a exclusive outerbound
i >= i+1; slice is i
i >= length(array) or i<0; raises an error
"""
np.add.reduceat(arr_multidim,[0,4,5,2,4,1],axis=3)
# Slices : [0:4],[4:5],[5],[2:4],[4],[1+]'

# Apply the ufunc operation to all pairs for every a in A and b in B
"""
np.ufunc.outer : Applies ufunc operation to every pair; Resulting array has shape A.shape + B.shape
Format         : np.ufunc.outer(array_A,arr_B)
If shape of array A is (x,y) and B is (l,m,n) ; resulting array will have shape (x,y,l,m,n)
"""
np.add.outer(arr_multidim[0,0,0],arr_multidim[0,1])
# Shape of resulting array is (6,2,6)

# Broadcasting
"""
Broadcasting is a set of rules for applying binary UFuncs on arrays of different sizes.

Rules of Broadcasting :

1. Rule 1 : If the 2 arrays differ in dimensions, the shape of the one with fewer dimensions is padded with 1s on its leading side
2. Rule 2 : If the shape of the 2 arrays does not match in any dimension, tha array with shape equal to 1 is stretched to match the other shape
3. Rule 3 : If neither sizes match in any dimension nor is 1, then an error is raised
"""

arr_brdcst1 = np.random.randint(40,size=(2,4,2,1))
arr_brdcst2 = np.random.randint(20,size=(2,3))
arr_brdcst3 = np.random.randint(20,size=(2,3,5))

arr_viable = arr_brdcst1 + arr_brdcst2
# Rule 1 : Smaller array arr_brdcst2 padded with 1s on left making its dimensions (1,1,2,3)
# Rule 2 : Due to dimension mismatch, both arr_brdcst1 and arr_brdcst2 are stretched/broadcasted
arr_viable.shape
# Forms an output array of dimensions (2,4,2,3)

arr_error_brdcst = arr_brdcst1 + arr_brdcst3
# Raises an error : operands could not be broadcast together with shapes (2,4,2,1) (2,3,5)

pyFiles/NumPyCodeFiles_Vectorized Computations & Broadcasting.py


f = lambda x: 0.1*x**5 - 0.2*x**3 + 0.1*x - 0.2

h = 0.05        # Step size, equal differences of x, ∆x
# h = 0.01      # Smaller step size gives less error
x = 0.1         # Find derivatives of f(x) at point x

## xi = x, x[i+1] = x+h, x[i+2] = x+2*h, x[i-2] = x-2*h

# Forward Differences Approximation:
dff1 = (f(x+h) - f(x))/h
dff2 = (f(x+2*h) - 2*f(x+h) + f(x))/h**2
print('Forward Differences Approximation:')
print('f\' (', x, ') = %f' % dff1)
print('f\'\'(', x, ') = %f' % dff2)

# Central Differences Approximation:
dfc1 = (f(x+h) - f(x-h))/(2*h)
dfc2 = (f(x+h) - 2*f(x) + f(x-h))/h**2
print('Central Differences Approximation:')
print('f\' (', x, ') = %f' % dfc1)
print('f\'\'(', x, ') = %f' % dfc2)

# Backward Differences Approximation:
dfb1 = (f(x) - f(x-h))/h
dfb2 = (f(x) - 2*f(x-h) + f(x-2*h))/h**2
print('Backward Differences Approximation:')
print('f\' (', x, ') = %f' % dfb1)
print('f\'\'(', x, ') = %f' % dfb2)

# Analytical Differentiation:
dfa1 = lambda x: 0.5*x**4 - 0.6*x**2 + 0.1
dfa2 = lambda x: 2.0*x**3 - 1.2*x
print('Analytical Differentiation:')
print('f\' (', x, ') = %f' % dfa1(x))
print('f\'\'(', x, ') = %f' % dfa2(x))

# ~~~~~~~~~~~~~~~~~~~~~~~~~ Plotting the function ~~~~~~~~~~~~~~~~~~~~~~~~~~

from numpy import linspace
from matplotlib.pyplot import plot, xlabel, ylabel, legend, grid, show

h = 0.01
X = linspace(0, 1, 11)          # Divides from 0 to 1 into 11 points

# Central Differences Approximation:
DFC1 = (f(X+h) - f(X-h))/(2*h)
DFC2 = (f(X+h) - 2*f(X) + f(X-h))/h**2

# pyplot.
plot(X,f(X),'-k', X,DFC1,'--b', X,DFC2,'-.r')
xlabel('x')
ylabel('y, y\', y\'\'')
legend(['f(x)', 'f\'(x)', 'f\'\'(x)'])
grid()
show()

"""
f'(x) = df(x)/dx = lim x→0 (∆f(x)/∆x)
f'(x) ≈ (f(x+∆x)-f(x))/∆x
f'(xi) ≈ (f(x[i+1])-f(xi))/h    where x = xi, x+∆x = x[i+1], ∆x = h

Taylor's series expansion of f(x):
f(x) = f(a) + f'(a)/1! (x-a) + f''(a)/2! (x-a)^2 + f'''(a)/3! (x-a)^3 + ...

The expansion of f(x[i+1]) will be: x = x[i+1], a = xi, x-a = x[i+1]-xi = h
f(x[i+1]) = f(xi) + f'(xi).h + f''(xi).h^2 / 2! + f'''(xi).h^3 / 3! + ...
f'(xi) = (f(x[i+1]) - f(xi))/h - f''(xi).h / 2! - f'''(xi).h^2 / 3! + ...
By omitting the terms containing second and higher derivatives:
f'(xi) = (f(x[i+1]) - f(xi))/h + O(h) ; O(h) is the error from truncation

(a) Forward Finite Differences:     Error O(h)
f'   (xi) = (f(x[i+1]) - f(xi))/h
f''  (xi) = (f(x[i+2]) - 2f(x[i+1]) + f(xi))/h^2
f''' (xi) = (f(x[i+3]) - 3f(x[i+2]) + 3f(x[i+1]) - f(xi))/h^3
f''''(xi) = (f(x[i+4]) - 4f(x[i+3]) + 6f(x[i+2]) - 4f(x[i+1]) + f(xi))/h^4

(b) Central Finite Differences:     Error O(h^2)
f'   (xi) = (f(x[i+1]) - f(x[i-1]))/2h
f''  (xi) = (f(x[i+2]) - 2f(xi) + f(x[i-1]))/h^2
f''' (xi) = (f(x[i+2]) - 2f(x[i+1]) + 2f(x[i-1]) - f(x[i-2]))/2h^3
f''''(xi) = (f(x[i+2]) - 4f(x[i+1]) + 6f(xi) - 4f(x[i-1]) + f(x[i-2]))/h^4

(c) Backward Finite Differences:    Error O(h)
f'   (xi) = (f(xi) - f(x[i-1]))/h
f''  (xi) = (f(xi) - 2f(x[i-1]) + f(x[i-2]))/h^2
f''' (xi) = (f(xi) - 3f(x[i-1]) + 3f(x[i-2]) - f(x[i-3]))/h^3
f''''(xi) = (f(xi) - 4f(x[i-1]) + 6f(x[i-2]) - 4f(x[i-3]) + f(x[i-4]))/h^4

"""

3. Numerical Differentiation/1. Finite Differences Approximations or Methods.py


import numpy as np

def euclidean_distances(X, Y):
    """Compute pairwise Euclidean distance between the rows of two matrices X (shape MxK)
    and Y (shape NxK). The output of this function is a matrix of shape MxN containing
    the Euclidean distance between two rows.

    Arguments:
        X {np.ndarray} -- First matrix, containing M examples with K features each.
        Y {np.ndarray} -- Second matrix, containing N examples with K features each.

    Returns:
        D {np.ndarray}: MxN matrix with Euclidean distances between rows of X and rows of Y.
    """

    M = X.shape[0]
    N = Y.shape[0]
    D = np.zeros([M, N])

    for i in range(M):
        for j in range(N):
            D[i, j] = np.sqrt(np.dot(X[i] - Y[j], X[i] - Y[j]))

    return D

def manhattan_distances(X, Y):
    """Compute pairwise Manhattan distance between the rows of two matrices X (shape MxK)
    and Y (shape NxK). The output of this function is a matrix of shape MxN containing
    the Manhattan distance between two rows.

    Arguments:
        X {np.ndarray} -- First matrix, containing M examples with K features each.
        Y {np.ndarray} -- Second matrix, containing N examples with K features each.

    Returns:
        D {np.ndarray}: MxN matrix with Manhattan distances between rows of X and rows of Y.
    """

    M = X.shape[0]
    N = Y.shape[0]
    D = np.zeros([M, N])

    for i in range(M):
        for j in range(N):
            temp = np.abs(X[i] - Y[j])
            D[i, j] = np.sum(temp)

    return D

def cosine_distances(X, Y):
    """Compute Cosine distance between the rows of two matrices X (shape MxK)
    and Y (shape NxK). The output of this function is a matrix of shape MxN containing
    the Cosine distance between two rows.

    Arguments:
        X {np.ndarray} -- First matrix, containing M examples with K features each.
        Y {np.ndarray} -- Second matrix, containing N examples with K features each.

    Returns:
        D {np.ndarray}: MxN matrix with Cosine distances between rows of X and rows of Y.
    """

    M = X.shape[0]
    N = Y.shape[0]
    D = np.zeros([M, N])

    for i in range(M):
        for j in range(N):
            n_1 = np.sqrt(np.dot(X[i], X[i]))
            n_2 = np.sqrt(np.dot(Y[j], Y[j]))
            # This yield the cosine value of the two vectors, which range from 1 to -1
            D[i, j] = np.dot(X[i], Y[j]) / (n_1 * n_2)

    # Larger value (closer to 1) means the two vectors have similar direction, so they
    # should have smaller distance
    return 1 - D


print('*'*15+'Plain numpy implementation of PCA'+'*'*15)
#Written using only numpy
from numpy import array,mean,cov
from numpy.linalg import eig
from sklearn.decomposition._pca import PCA

#define matrix
print("\n")
A = array([[1,2],[3,4],[5,6]])
print("Initial Martix = {} \n".format(A))

#Calculate mean
M = mean(A.T,axis = 1)
print("Mean of the matrix = {} \n".format(M))

#Scale columns by subtracting column means
C = A - M
print("Column scaling applied over the matrix = {} \n".format(C))

#Calculate covariance
V = cov(C.T)
print("Covariance of the matrix = {} \n".format(V))

values,vectors = eig(V)
print("Eigen Vectors = {} \n".format(vectors))
print("Eigen values = {} \n".format(values))

P = vectors.T.dot(C.T)
print("Matrix after applying PCA = {} \n".format(P.T))

#Scikit learn Verifcation of the above result
print('*'*15+'Verification of the above result using sklearn'+'*'*15)

from numpy import array
from sklearn.decomposition import PCA
# define a matrix
A = array([[1, 2], [3, 4], [5, 6]])
print(A)
# create the PCA instance
pca = PCA(2)
# fit on data
pca.fit(A)
# access values and vectors
print(pca.components_)
print(pca.explained_variance_)
# transform data
B = pca.transform(A)
print(B)
"""
***************Plain numpy implementation of PCA***************
Initial Martix = [[1 2]
 [3 4]
 [5 6]]
Mean of the matrix = [3. 4.]
Column scaling applied over the matrix = [[-2. -2.]
 [ 0.  0.]
 [ 2.  2.]]
Covariance of the matrix = [[4. 4.]
 [4. 4.]]
Eigen Vectors = [[ 0.70710678 -0.70710678]
 [ 0.70710678  0.70710678]]
Eigen values = [8. 0.]
Matrix after applying PCA = [[-2.82842712  0.        ]
 [ 0.          0.        ]
 [ 2.82842712  0.        ]]
***************Verification of the above result***************
[[1 2]
 [3 4]
 [5 6]]
[[ 0.70710678  0.70710678]
 [-0.70710678  0.70710678]]
[8. 0.]
[[-2.82842712e+00 -2.22044605e-16]
 [ 0.00000000e+00  0.00000000e+00]
 [ 2.82842712e+00  2.22044605e-16]]
"""


import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt

###### generate some data ###########
X = np.linspace(-1, 1, 100)[:, np.newaxis]
np.random.shuffle(X)    # randomize the data
# Y =  1 * X**3 + 0.5 * X**2 + 1 + np.random.normal(0, 0.01, (100, 1)) #曲線
Y = 3 * X + 2 + np.random.normal(0, 0.05, (100, 1))  # 線性
X_train, Y_train = X[:70], Y[:70]     # train 前 60 data points
X_test, Y_test = X[70:], Y[70:]

dim_input = 1
dim_output = 1
###########  Linear Regression by TensorFlow (GradientDescent) ###############
xs = tf.placeholder(tf.float32, [None, dim_input], name='x_input')
ys = tf.placeholder(tf.float32, [None, dim_output], name='y_output')
# prediction = xs * w + b
w = tf.Variable(tf.random_normal([dim_input, dim_output]))
b = tf.Variable(tf.zeros([1, dim_output]) + 1,)
prediction = tf.matmul(xs, w) + b
# loss function (prediction and real target)
loss = tf.reduce_mean(tf.reduce_sum(
    tf.square(prediction-ys), reduction_indices=[1]), name='Loss')
# optimal: find the Weights and biases
train_step = tf.train.GradientDescentOptimizer(0.1).minimize(loss)
sess = tf.Session()
sess.run(tf.global_variables_initializer())
#############################################

######## Learning #####################
mse = np.array([])
for i in range(1000):
    # training
    sess.run(train_step, feed_dict={xs: X_train, ys: Y_train})
    y_pre = sess.run(prediction, feed_dict={xs: X_test})
    mse = np.append(mse, np.mean(np.sqrt(np.sum(np.square(Y_test-y_pre)))))
    if i >= 1:
        if abs(mse[i]-mse[i-1]) < np.spacing(1):
            break
    print('Iternation' + str(i+1) + ': MSE=' + str(mse[i]))
mse_tf = mse[-1]
plt.plot(mse)
plt.xlabel('Iternation time')
plt.ylabel('MSE')
plt.title('MSE')
plt.show()

####### Least square error Regression by closedform ########
#最小平方法即是將
# beta=(X^TX)^-1 X^T Y
def Train_LSE_Regression(x, y):
    tmp1 = np.matmul(np.transpose(x), x)
    tmp1 = np.linalg.pinv(tmp1)
    tmp2 = np.matmul(np.transpose(y), x)
    Weight = np.matmul(tmp1, np.transpose(tmp2))
    return Weight

n_train = np.size(X_train, 0)
n_test = np.size(X_test, 0)
X_input_train = np.concatenate((X_train, np.ones([n_train, 1])), axis=1)
Weight_LSE = Train_LSE_Regression(X_input_train, Y_train)

X_input_test = np.concatenate((X_test, np.ones([n_test, 1])), axis=1)
y_pre_LSE = np.matmul(X_input_test, Weight_LSE)
mse_LSE = np.mean(np.sqrt(np.sum(np.square(Y_test-y_pre))))

# compared the weight between these two methods
print('TF (GD) Weights:' + str(sess.run(w)) + str(sess.run(b)))
print('TF (GD) MSE:' + str(mse_tf))
print('LSE     Weights:' + str(Weight_LSE[0]) + str(Weight_LSE[1]))
print('LSE     MSE:' + str(mse_LSE))

### plot generated data######
fig = plt.figure()
ax = fig.add_subplot(2, 1, 1)
ax.scatter(X_train, Y_train)
ax.set_xlabel('Input data')
ax.set_ylabel('Target result')
ax.set_title('Train data')
plt.ion()  # can hold on
ax = fig.add_subplot(2, 1, 2)
ax.scatter(X_test, Y_test)
ax.scatter(X_test, y_pre, color="r")
ax.scatter(X_test, y_pre_LSE, color="b")
ax.set_xlabel('Input data')
ax.set_ylabel('Target result')
ax.set_title('Test data')
plt.show()

'''
結論:
從結果可以發現，梯度法執行到收斂後找到的答案跟唯一解找出來的答案基本上一致。
雖然唯一解很快，只需要矩陣運算計算一組即可以得到答案
但因為唯一解在高維度低樣本數時可能會有共變異數矩陣(covariance matrix)奇異(singular)的情況出現
這時候可能會造成估計錯誤，造成不良的結果
所以最好實際上在算的時候，還是用梯度法去執行Regression。

我的理解：
用最小平方法即是找到圖形中微分等於零的點，但因為微分等於零有可能是局部較佳解，也有可能是平坦的面，所以用梯度。
梯度下降是找偏微分(梯度)，wb減去其值。而最小平方法是令梯度等於零(最低、最高點)，找出wb
'''


"""
demo_nonlinear_data_fitting.py

Fit a model A * sin(W * t + phi) to the data f(X, ti) = yi to find A, W, and phi
m = number of data points

Solve a system of non-linear equations f(X, ti) - yi = 0:
    x1 * sin(x2 * t + x3) - y = 0, where X = [x1 = A, x2 = W, x3 = phi].T, t = [t1, t2, ..., tm].T and y = [y1, y2, ..., ym].T

Minimize the following objective function: (f(X, t) - y).T @ (f(X, t) - y)

Levenberg - Marquardt algorithm
    General algorithm for any f(X) function. Requires residuals and jacobian.
    X0 converges to X* for any X0.

Naive random walk algorithm
    General algorithm for any f(X) function. Requires only residuals.
    X0 converges to X* for any X0.

Simulated annealing algorithm
    General algorithm for any f(X) function. Requires only residuals.
    X0 converges to X* for any X0.

Particle swarm optimization algorithm
    General algorithm for any f(X) function. Requires only residuals.
    X0 converges to X* for any X0.
"""

import numpy as np
import matplotlib.pyplot as plt
from levenberg_marquardt_algorithm import levenberg_marquardt_algorithm
from naive_random_search_algorithm import naive_random_search_algorithm
from simulated_annealing_algorithm import simulated_annealing_algorithm
from particle_swam_optimization_algorithm import particle_swam_optimization_algorithm
from print_report import print_report
from plot_progress_y import plot_progress_y
import time

X = np.array([[0.75], [2.3], [-3]]);
t = np.arange(0, 20, 0.1);
# Nonlinear model to fit
func = lambda X : X[0] * np.sin(X[1] * t + X[2]);
# Plot the curve
fig = plt.figure();
y = func(X);
plt.plot(t, y)
plt.show()

func_residual = lambda X : (X[0] * np.sin(X[1] * t + X[2]) - y).reshape((t.size, 1));
func_jacobian = lambda X : np.array([[-np.sin(X[1] * t + X[2])], [-t * X[0] * np.cos(X[1] * t + X[2])], [-X[0] * np.cos(X[1] * t + X[2])]]).reshape((t.size, X.size));
# Objective function for naive random walk and simulated annealing algorithms
func_error = lambda X : np.linalg.norm((X[0] * np.sin(X[1] * t + X[2]) - y).reshape((t.size, 1)), axis = 0) ** 2;
# Objective function for particle swarm optimization algorithm (particles along row dimension, axis = 0)
func_error_ps = lambda X : np.linalg.norm(X[:, [0]] * np.sin(X[:, [1]] * t + X[:, [2]]) - y, axis = 1).reshape((-1, 1)) ** 2;

# Levenberg-Marquardt algorithm
print('***********************************************************************');
print('Levenberg - Marquardt algorithm');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
X_lower = np.array([[0], [0], [-5]]); # X lower bound
X_upper = np.array([[2], [5], [0]]); # X upper bound
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max, 'x_lower' : X_lower,
           'x_upper' : X_upper};
X0 = np.array([[0.1], [1], [-2]]);
start = time.time();
X, report = levenberg_marquardt_algorithm(X0, func_residual, func_jacobian, options);
end = time.time();
print_report(func_error, report);
# Plot path to X* for Y
algorithm_name = 'Levenberg - Marquardt algorithm';
plot_progress_y(algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Naive random walk algorithm
print('***********************************************************************');
print('Naive random walk algorithm');
N_iter_max = 1000;
tolerance_x = 10e-8;
tolerance_y = 10e-8;
X_lower = np.array([[0], [0], [-5]]); # X lower bound
X_upper = np.array([[2], [5], [0]]); # X upper bound
alpha = 0.5; # step size
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max, 'x_lower' : X_lower,
           'x_upper' : X_upper, 'alpha' : alpha};
X0 = np.array([[0.1], [1], [-2]]); # X0 = X_lower + (X_upper - X_lower) * np.random.rand(X_lower.size, 1);
start = time.time();
X, report = naive_random_search_algorithm(X0, func_error, options);
end = time.time();
print_report(func_error, report);
# Plot path to X* for Y
algorithm_name = 'Naive random walk algorithm';
plot_progress_y(algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Simulated annealing algorithm
print('***********************************************************************');
print('Simulated annealing algorithm');
N_iter_max = 1000;
tolerance_x = 10e-8;
tolerance_y = 10e-8;
X_lower = np.array([[0], [0], [-5]]); # X lower bound
X_upper = np.array([[2], [5], [0]]); # X upper bound
alpha = 1.0; # step size
gamma = 1.5; # controls temperature decay, gamma > 0
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max, 'x_lower' : X_lower,
           'x_upper' : X_upper, 'alpha' : alpha, 'gamma' : gamma};
X0 = np.array([[0.1], [1], [-2]]); # X0 = X_lower + (X_upper - X_lower) * np.random.rand(X_lower.size, 1);
start = time.time();
X, report = simulated_annealing_algorithm(X0, func_error, options);
end = time.time();
print_report(func_error, report);
# Plot path to X* for Y
algorithm_name = 'Simulated annealing algorithm';
plot_progress_y(algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Particle swarm optimization algorithm
print('***********************************************************************');
print('Particle swarm optimization algorithm');
N_iter_max = 1000;
tolerance_x = 10e-8;
tolerance_y = 10e-8;
X_lower = np.array([[0], [0], [-5]]); # X lower bound
X_upper = np.array([[2], [5], [0]]); # X upper bound
d_lower = -1; # direction (aka velocity) lower bound
d_upper = 1; # direction (aka velocity) upper bound
N_ps = 10000; # number of particles
w = 0.9; # inertial constant, w < 1
c1 = 1.5; # cognitive/independent component, c1 ~ 2
c2 = 1.5; # social component, c2 ~ 2
alpha = 1; # step size
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max, 'x_lower' : X_lower,
           'x_upper' : X_upper, 'alpha' : alpha, 'd_lower' : d_lower, 'd_upper' : d_upper, 'N_ps' : N_ps, 'w' : w, 'c1' : c1, 'c2' : c2};
start = time.time();
X, report = particle_swam_optimization_algorithm(func_error_ps, options);
end = time.time();
print_report(func_error, report);
# Plot path to X* for Y
algorithm_name = 'Particle swarm optimization algorithm';
plot_progress_y(algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');


from functools import reduce

import numpy as np

# Dynamic programming solution from CLRS 15.2
def matrix_chain_order(p):
  n = len(p) - 1
  m = np.zeros(shape=[n, n])
  s = np.zeros(shape=[n, n])
  for l in range(2, n + 1):
    for i in range(0, n - l + 1):
      j = i + l - 1
      m[i, j] = np.Inf
      for k in range(i, j):
        q = m[i, k] + m[k + 1, j] + p[i] * p[k + 1] * p[j + 1]
        if q < m[i, j]:
          m[i, j] = q
          s[i, j] = k
  m = m.astype(int)
  s = s.astype(int)
  return m, s

# Print the optimal parenthesisation
def optimal_parenthesis(s, i, j, print=True):
  if i == j:
    return 'A{}'.format(i + 1)
  else:
    return '({}{})'.format(optimal_parenthesis(s, i, s[i, j]),
                            optimal_parenthesis(s, s[i, j] + 1, j))

# "Dumb" chain_multiply
def chain_multiply(arrays):
  return reduce(np.dot, arrays)

# Recursive chain_multiply using the DP optimal solution
def chain_multiply_dp(arrays):
  p = [x.shape[0] for x in arrays]
  p.append(arrays[-1].shape[1])
  m, s = matrix_chain_order(p)

  def chain_multiply_dp_aux(i, j):
    if i == j:
      return arrays[i]
    else:
      return np.dot(chain_multiply_dp_aux(i, s[i, j]),
                       chain_multiply_dp_aux(s[i, j] + 1, j))

  return chain_multiply_dp_aux(0, len(p) - 2)

# To generate a bunch of random arrays for benchmarking
def generate_arrays(p):
  rng = np.random.default_rng()
  return [rng.random((dim1, dim2)) for dim1, dim2 in zip(p[:-1], p[1:])]

# Stuff from CLRS
p = [30, 35, 15, 5, 10, 20, 25]
m, s = matrix_chain_order(p)
m[0, 5] # min number of scalar multiplications
optimal_parenthesis(s, 0, 5) # optimal parenthesisation

# Try to do the actual multiplications with random arrays
arrays_random = generate_arrays(p)

prod0 = chain_multiply(arrays_random)
prod1 = np.linalg.multi_dot(arrays_random)
prod2 = chain_multiply_dp(arrays_random)
np.linalg.norm(prod0 - prod1) # floating point precision
np.linalg.norm(prod1 - prod2)

# Increase the dimension for benchmarking
p10 = [x * 10 for x in p]
arrays_random = generate_arrays(p10)

%timeit chain_multiply(arrays_random)
%timeit np.linalg.multi_dot(arrays_random)
%timeit chain_multiply_dp(arrays_random)


import numpy as np
from matplotlib import pyplot
import math
#from mplkit.mplot3d import axes3d

# Load the data
data = np.loadtxt('linear_regression.txt', delimiter = ',')
#separate predictor from target variable
xd = np.c_[np.ones(data.shape[0]), data[:,0]]
yd = np.c_[data[:,1]]
print(yd.shape)
print(xd.shape)

# First appraoch - Normal equation

def normalEquation(x,y):
    """
    Parameteres: input variables (Table) , Target vector
    Instructions: Complete the code to compute the closed form solution to linear regression and 	save the result in theta.
    Return: coefficinets
    """
    theta=[]
    x_t=np.transpose(x)
    theta=np.linalg.inv(x_t.dot(x))
    theta=theta.dot(x_t)
    theta=theta.dot(y)
    return theta

# Iterative Approach - Gradient Descent

'''
Following paramteres need to be set by you - you may need to run your code multiple times to find the best combination
'''
theta=np.matrix([[0],[0]])
i=1000
r=0.01

def gradient_descent(x,y,theta,i,r):
    """
    Paramters: input variable , Target variable, theta, number of iteration, learning_rate
    Instructions: Complete the code to compute the iterative solution to linear regression, in each iteration you will
    add the cost of the iteration to a an empty list name cost_hisotry and update the theta.
    Return: theta, cost_history
    """
    n=float(len(y))
    cost_history = []
    theta1=[]
    theta2=[]
    # Your code goes here
    x_t=np.transpose(x)
    for i in range(0,i):
        theta1.append(theta.item(0,0))
        theta2.append(theta.item(1,0))
        ytrain=x.dot(theta)
        ydiff=y-ytrain
        cost=(np.transpose(ydiff).dot(ydiff))/(2*n)
        cost_history.append(float(cost))
        thetagrad=((x_t).dot(ydiff))/n
        theta=theta+r*(thetagrad)
    return theta, cost_history,theta1,theta2

print(gradient_descent(xd,yd,theta,i,r)[0])
print(normalEquation(xd,yd))

# Plot the cost over number of iterations

#Your plot should be similar to the provided plot

x=range(0,1000)
pyplot.plot(x,gradient_descent(xd,yd,theta,i,r)[1])
pyplot.show()
"""
theta, cost_history,theta1,theta2=gradient_descent(xd,yd,theta,i,r)

# Plot the linear regression line for both gradient approach and normal equation in same plot
'''
hints: your x-axis will be your predictor variable and y-axis will be your target variable. plot a
scatter plot and draw the regression line using the theta calculated from both approaches. Your plot
should be similar to what provided.
'''

# Plot contour plot and 3d plot for the gradient descent approach

'''
your plots should be similar to our plots.

'''

#pyplot.contour(theta1, theta2, cost_history, colors='black');

"""
pyplot.plot(xd,yd,'ro')
pyplot.plot(xd,xd.dot(normalEquation(xd,yd)))
pyplot.plot(xd,xd.dot((gradient_descent(xd,yd,theta,i,r))[0]))
pyplot.show()


import scipy.special
import numpy as np
import matplotlib

def function(x0):
    return np.sin(x0)

def function_derivative(x0):
    return np.cos(x0)

x0 = 1.2 ## point that we compute the derivative at (ie d/dx sin(x) at x = x0)
f0 = function(x0) ## f(x0)
fp = function_derivative(x0) ## f'(x0) the `p` means 'prime'
fpp = -function(x0) ## f''(x0)
i = linspace(-20, 0, 40) ## `linspace` gives a range of values between two end points
##                          in this case 40 points, between -20 and 0
h = 10.0**i ## this is our approx parameter, it is an array of values
##             between 10^(-20) and 10^(0)

# Remember we are using the forward difference formula
fp_approx = (sin(x0 + h) - f0)/h ## the derivative approximation

# To use the central difference formula
# Absolute error in 0(h^2) as long as f'''(x) is bounded
# fp_approx = (sin(x0 + h) - sin(x0 - h))/(2*h)

# To use the backward difference formula
# fp_approx = (f0 - sin(x0 - h))/h

# Forward difference formula
err = absolute(fp - fp_approx) ## the full absolute error
d_err = h/2*absolute(fpp) ## the formula for the discretization error, derived above

figure(1, [7, 5]) ## creates a blank figure 7 inches (wide) by 5 inches (height)
loglog(h, err, '-*') ## makes a plot with a log scale on both the x and y axis
loglog(h, d_err, 'r-', label=r'$\frac{h}{2}\vert f^{\prime\prime}(x) \vert $')
xlabel('h', fontsize=20) ## puts a label on the x axis
ylabel('absolute error', fontsize=20) ## puts a label on the y axis
ylim(1e-15, 1) ## places limits on the yaxis for our plot
legend(fontsize=24); ## creates a figure legend (uses the `label=...` arguments in the plot command)

# notes : For  ℎ  small but not too small, the absolute error is dominated by the discretization error,
# ℎ|𝑓″(𝑥)|/2 , which is larger than other sources of error such as roundoff error.
#  Once  ℎ<10−8 , the discretization error becomes smaller than the roundoff error, and the roundoff error continues to get larger as  ℎ→0 .

# Centered Difference Formula
x0 = 1.2 ## point that we compute the derivative at (ie d/dx sin(x) at x = x0)
f0 = sin(x0) ## f(x0)
fp = cos(x0) ## f'(x0) the `p` means 'prime'
fpp = -sin(x0) ## f''(x0)
fppp = -cos(x0) ## f'''(x0)
i = linspace(-20, -1, 40) ## `linspace` gives a range of values between two end points
##                          in this case 40 points, between -20 and 0
h = 10.0**i ## this is our approx parameter, it is an array of values
##             between 10^(-20) and 10^(0)
fp_approx_q1 = (sin(x0 + h) - sin(x0 - h))/(2*h) ## the derivative approximation
fp_approx_example = (sin(x0 + h) - f0)/h
print("Derivative Approximation " , fp_approx_q1)

err = absolute(fp - fp_approx_q1) ## the full absolute error
err1 = absolute(fp - fp_approx_example)
print("Absolute Error", err)
d_err_q1 = (h**2/6)*absolute(fppp) ## the formula for the discretization error, derived above
d_err_example = (h/2) * absolute(fpp)
print("Exact Derivative",d_err)

# Comparison Plots of the two methods
plt.figure(1, [10, 5])
loglog(h, err, '-*') ## makes a plot with a log scale on both the x and y axis
loglog(h, d_err_q1, 'r-', label=r'$\frac{h^2}{6}\vert f^{\prime\prime\prime}(x) \vert $')
plt.title("Q1 - Derivative Approximation")
xlabel('h', fontsize=20) ## puts a label on the x axis
ylabel('absolute error', fontsize=20) ## puts a label on the y axis
ylim(1e-15, 1) ## places limits on the yaxis for our plot
legend(fontsize=24); ## creates a figure legend

plt.figure(2, [10,5])
loglog(h, err1, '-*') ## makes a plot with a log scale on both the x and y axis
loglog(h, d_err_example, 'r-', label=r'$\frac{h}{2}\vert f^{\prime\prime}(x) \vert $')
plt.title("Example 2 - Derivative Approximation")
xlabel('h', fontsize=20) ## puts a label on the x axis
ylabel('absolute error', fontsize=20) ## puts a label on the y axis
ylim(1e-15, 1) ## places limits on the yaxis for our plot
legend(fontsize=24); ## creates a figure legend


# Use the following code for the exercises below:

import numpy as np

a = np.array([4, 10, 12, 23, -2, -1, 0, 0, 0, -6, 3, -7])

#1  How many negative numbers are there?

print(f'There are {len(a[a < 0])} negative numbers in this array.')

    # There are 4 negative numbers in this array.

#________________________________________________________________________

#2  How many positive numbers are there?

print(f'There are {len(a[a > 0])} negative numbers in this array.')

    # There are 5 negative numbers in this array.
#________________________________________________________________________

#3  How many even positive numbers are there?

#create array of positive numbers from a
positives = a[a > 0]

#use array filtering to filter only even numbers  ###can use (& and) or (| or)
even_and_positives = positives[positives % 2 == 0]

print(f'There are {len(even_and_positives)} numbers that are both even and positive.')

    # There are 3 numbers that are both even and positive.

#ANOTHER WAY:

even_positive_numbers = a[(a > 0) & (a % 2 == 0)]
even_positive_numbers
#________________________________________________________________________

#4  If you were to add 3 to each data point, how many positive numbers would there be?

# use vectorizing operation to add 3 to each element in the array
plus_three = a + 3

# use array filtering to filter only even numbers
positive_and_plus_three = plus_three[plus_three > 0]

print(f'After we add 3 to each data point, there are {len(positive_and_plus_three)} positive numbers.')

#________________________________________________________________________

#5  If you squared each number, what would the new mean and standard deviation be?

#create new array with squared elements
squared_elements = a ** 2

#use array method .mean() to calculate the mean
print(f'The mean of the elements squared is: {squared_elements.mean()}')

#use array method .std() to calculate standard deviation
print(f'The standard deviation of the elements squared is: {squared_elements.std()}')

    #The mean of the elements squared is: 74.0
        #The standard deviation of the elements squared is: 144.0243035046516

#________________________________________________________________________

#6  A common statistical operation on a dataset is centering. This means to adjust the data such that the mean of the data is 0.
# This is done by subtracting the mean from each data point. Center the data set.

# use vectorizing operation to subtract mean from each element of array a
centered_data_set = a - a.mean()
print(centered_data_set)

    #[  1.   7.   9.  20.  -5.  -4.  -3.  -3.  -3.  -9.   0. -10.]

#________________________________________________________________________

 #7  Calculate the z-score for each data point.

 #use vectoring operation to divide centered data set by standard deviation
 z_scores = centered_data_set / a.std()
 print(z_scores)

    # [ 0.12403473  0.86824314  1.11631261  2.48069469 -0.62017367 -0.49613894
    #  -0.3721042  -0.3721042  -0.3721042  -1.11631261  0.         -1.24034735]

 #________________________________________________________________________

 #8  MORE NUMPY PRACTICE:

 import numpy as np
# Life w/o numpy to life with numpy

## Setup 1
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Use python's built in functionality/operators to determine the following:

# Exercise 1 - Make a variable called sum_of_a to hold the sum of all the numbers in above list

sum_of_a = sum(a)
print(sum_of_a)  #55

# Exercise 2 - Make a variable named min_of_a to hold the minimum of all the numbers in the above list

min_of_a = min(a)
print(min_of_a)  #1

# Exercise 3 - Make a variable named max_of_a to hold the max number of all the numbers in the above list

max_of_a = max(a)
print(max_of_a)   #10

# Exercise 4 - Make a variable named mean_of_a to hold the average of all the numbers in the above list

mean_of_a = sum(a) / len(a)
print(mean_of_a)   #5.5

# Exercise 5 - Make a variable named product_of_a to hold the product of multiplying all the numbers in the above list together

product_of_a = 1
for n in a:
    product_of_a *= n
print(product_of_a)  #3628800

# Exercise 6 - Make a variable named squares_of_a. It should hold each number in a squared like [1, 4, 9, 16, 25...]

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

squares_in_a = []
for i in a:
    squares_in_a.append(i ** 2)
print(squares_in_a)   # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# Exercise 7 - Make a variable named odds_in_a. It should hold only the odd numbers

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

odds_in_a = []
for i in a:
    if i % 2 == 1:
        odds_in_a.append(i)
print(odds_in_a)   # [1, 3, 5, 7, 9]

# Exercise 8 - Make a variable named evens_in_a. It should hold only the evens.

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

evens_in_a = []
for i in a:
    if i % 2 == 0:
        evens_in_a.append(i)
print(evens_in_a)   # [2, 4, 6, 8, 10]

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

## What about life in two dimensions? A list of lists is matrix, a table, a spreadsheet, a chessboard...
## Setup 2: Consider what it would take to find the sum, min, max, average, sum, product, and list of squares for this list
# of two lists.

b = [
    [3, 4, 5],
    [6, 7, 8]
]

# Exercise 1 - refactor the following to use numpy. Use sum_of_b as the variable. **Hint, you'll first need to make sure that the "b" variable is a numpy array**
# sum_of_b = 0
# for row in b:
#     sum_of_b += sum(row)

b = np.array(b)

b.sum()  #33

# Exercise 2 - refactor the following to use numpy.
# min_of_b = min(b[0]) if min(b[0]) <= min(b[1]) else min(b[1])

b.min() #3

# Exercise 3 - refactor the following maximum calculation to find the answer with numpy.
# max_of_b = max(b[0]) if max(b[0]) >= max(b[1]) else max(b[1])

b.max() #8

# Exercise 4 - refactor the following using numpy to find the mean of b
# mean_of_b = (sum(b[0]) + sum(b[1])) / (len(b[0]) + len(b[1]))

b.mean() #5.5

# Exercise 5 - refactor the following to use numpy for calculating the product of all numbers multiplied together.
# product_of_b = 1
# for row in b:
#     for number in row:
#         product_of_b *= number

b.prod()  #20160

# Exercise 6 - refactor the following to use numpy to find the list of squares
# squares_of_b = []
# for row in b:
#     for number in row:
#         squares_of_b.append(number**2)

b ** 2
# [[ 9 16 25]
#  [36 49 64]]

# Exercise 7 - refactor using numpy to determine the odds_in_b
# odds_in_b = []
# for row in b:
#     for number in row:
#         if(number % 2 != 0):
#             odds_in_b.append(number)

 b[b % 2 != 0]  # array([3, 5, 7])

# Exercise 8 - refactor the following to use numpy to filter only the even numbers
# evens_in_b = []
# for row in b:
#     for number in row:
#         if(number % 2 == 0):
#             evens_in_b.append(number)

b[b % 2 == 0]  #array([4, 6, 8])

# Exercise 9 - print out the shape of the array b.
b.shape  #(2, 3)

# Exercise 10 - transpose the array b.
b.T

    # array([[3, 6],
    #        [4, 7],
    #        [5, 8]])

# Exercise 11 - reshape the array b to be a single list of 6 numbers. (1 x 6)

b.reshape(6)  #array([3, 4, 5, 6, 7, 8])

# Exercise 12 - reshape the array b to be a list of 6 lists, each containing only 1 number (6 x 1)

b.reshape(6,1)
    # array([[3],
    #        [4],
    #        [5],
    #        [6],
    #        [7],
    #        [8]])

##________________________________
## Setup 3
c = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# HINT, you'll first need to make sure that the "c" variable is a numpy array prior to using numpy array methods.
# Exercise 1 - Find the min, max, sum, and product of c.
c = np.array(c)

c.min(), c.max(), c.sum(), c.prod()  #(1, 9, 45, 362880)

# Exercise 2 - Determine the standard deviation of c.
c.std()  #2.581988897471611

# Exercise 3 - Determine the variance of c.
c.var()  #6.666666666666667

# Exercise 4 - Print out the shape of the array c
c.shape()  #(3, 3)

# Exercise 5 - Transpose c and print out transposed result.
c.T
    #array([[1, 4, 7],
    #    [2, 5, 8],
    #    [3, 6, 9]])

# Exercise 6 - Get the dot product of the array c with c.

np.dot(c, c).sum()   #729

# Exercise 7 - Write the code necessary to sum up the result of c times c transposed. Answer should be 261

(np.dot(c, c.T)).sum()   #261

# Exercise 8 - Write the code necessary to determine the product of c times c transposed. Answer should be 131681894400.

## Setup 4
d = [
    [90, 30, 45, 0, 120, 180],
    [45, -90, -30, 270, 90, 0],
    [60, 45, -45, 90, -45, 180]
]

# Exercise 1 - Find the sine of all the numbers in d

np.sin(d)

    # array([[ 0.89399666, -0.98803162,  0.85090352,  0.        ,  0.58061118,
    #         -0.80115264],
    #        [ 0.85090352, -0.89399666,  0.98803162, -0.17604595,  0.89399666,
    #          0.        ],
    #        [-0.30481062,  0.85090352, -0.85090352,  0.89399666, -0.85090352,
    #         -0.80115264]])

# Exercise 2 - Find the cosine of all the numbers in d

np.cos(d)

    # array([[-0.44807362,  0.15425145,  0.52532199,  1.        ,  0.81418097,
    #         -0.59846007],
    #     [ 0.52532199, -0.44807362,  0.15425145,  0.98438195, -0.44807362,
    #         1.        ],
    #     [-0.95241298,  0.52532199,  0.52532199, -0.44807362,  0.52532199,
    #         -0.59846007]])

# Exercise 3 - Find the tangent of all the numbers in d

np.tan(d)

    # array([[-1.99520041, -6.4053312 ,  1.61977519,  0.        ,  0.71312301,
    #         1.33869021],
    #     [ 1.61977519,  1.99520041,  6.4053312 , -0.17883906, -1.99520041,
    #         0.        ],
    #     [ 0.32004039,  1.61977519, -1.61977519, -1.99520041, -1.61977519,
    #         1.33869021]])

# Exercise 4 - Find all the negative numbers in d

d = np.array(d)
d[d < 0]  #array([-90, -30, -45, -45])

# Exercise 5 - Find all the positive numbers in d

d[d > 0] # array([ 90,  30,  45, 120, 180,  45, 270,  90,  60,  45,  90, 180])

# Exercise 6 - Return an array of only the unique numbers in d.

np.unique(d)  #array([-90, -45, -30,   0,  30,  45,  60,  90, 120, 180, 270])

# Exercise 7 - Determine how many unique numbers there are in d.

len(np.unique(d))    #11

# Exercise 8 - Print out the shape of d.

d.shape # (3, 6)

# Exercise 9 - Transpose and then print out the shape of d.

d.T.shape #(6,3)

# Exercise 10 - Reshape d into an array of 9 x 2

d.reshape(9, 2)

    #array([[ 90,  30],
    #      [ 45,   0],
    #      [120, 180],
    #      [ 45, -90],
    #      [-30, 270],
    #      [ 90,   0],
    #      [ 60,  45],
    #      [-45,  90],
    #      [-45, 180]])


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

def f(x): # Function whose derivative we need to calculate
	return np.sin(x)

def Df(x): # Exact derivative of f. This is used to compute error of the finite differences methods.
	return np.cos(x)

############################################
# Functions to calculate forward (1st, 2nd and 3rd Order), backward(1st, 2nd and 3rd Order) and central(2nd and 4th Order)
# difference based derivatives of a function f at x
# with h=dx as the differnce between x_i and x_i+1.
# The formulas for first, second and third order have been derived in the report.
############################################

def forward_difference(f, x, dx):

	# 1st order
	derivative_forward_1 = (f(x+dx) - f(x))/dx

	# 2nd order
	derivative_forward_2 = (-f(x+2*dx) + 4*f(x+dx) - 3*f(x))/(2*dx)

	# 3rd order
	derivative_forward_3 = (2*f(x+3*dx) - 9*f(x+2*dx) + 18*f(x+dx) - 11*f(x))/(6*dx)

	return derivative_forward_1, derivative_forward_2, derivative_forward_3

def backward_difference(f, x, dx):

	# 1st order
	derivative_backward_1 = (f(x) - f(x-dx))/dx

	# 2nd order
	derivative_backward_2 = (3*f(x) - 4*f(x-dx) + f(x-2*dx))/(2*dx)

	# 3rd order
	derivative_backward_3 = (4*f(x) - 7*f(x-dx) + 4*f(x-2*dx) - f(x-3*dx))/(2*dx)

	return derivative_backward_1, derivative_backward_2, derivative_backward_3

def central_difference(f, x, dx):

	# 2nd order
	derivative_central_2 = (f(x+dx) - f(x-dx))/(2*dx)

	# 4th order
	derivative_central_4 = (-f(x+2*dx) + 8*f(x+dx) - 8*f(x-dx) + f(x-2*dx))/(12*dx)

	return derivative_central_2, derivative_central_4

saved_graphs_dir = "Graphs-Finite_Differences/" # Folder to save the graphs
if not os.path.exists(saved_graphs_dir):
	os.mkdir(saved_graphs_dir)

# Define the dictionary to store errors of forward, backward and central difference methods for different orders
forward_difference_error = {
	"1st Order" : [],
	"2nd Order": [],
	"3rd Order": []
}
backward_difference_error = {
	"1st Order" : [],
	"2nd Order": [],
	"3rd Order": []
}
central_difference_error = {
	"2nd Order": [],
	"4th Order": []
}

x = 1 # Value whose derivative is to be evaluated
dx_range = [] # Store the range of dx values. Used to plot the error values vs dx.
exact_value = Df(x) # Exact value used to compute error.

for i in range(30):
	dx = 1/(2**i) # Half the value of dx after every iteration
	# print(dx)
	dx_range.append(dx)
	f1, f2, f3 = forward_difference(f, x, dx)
	# print((f1-exact_value)/exact_value)
	forward_difference_error["1st Order"].append(abs(f1-exact_value)/exact_value)
	forward_difference_error["2nd Order"].append(abs(f2-exact_value)/exact_value)
	forward_difference_error["3rd Order"].append(abs(f3-exact_value)/exact_value)

	b1, b2, b3 = backward_difference(f, x, dx)
	backward_difference_error["1st Order"].append(abs(b1-exact_value)/exact_value)
	backward_difference_error["2nd Order"].append(abs(b2-exact_value)/exact_value)
	backward_difference_error["3rd Order"].append(abs(b3-exact_value)/exact_value)

	c2, c4 = central_difference(f, x, dx)
	central_difference_error["2nd Order"].append(abs(c2-exact_value)/exact_value)
	central_difference_error["4th Order"].append(abs(c4-exact_value)/exact_value)

############################################
# Plot Error vs dx graphs in Normal scaling
############################################

plt.plot(dx_range, forward_difference_error["1st Order"])
plt.plot(dx_range, forward_difference_error["2nd Order"])
plt.plot(dx_range, forward_difference_error["3rd Order"])
plt.legend(["1st Order", "2nd Order", "3rd Order"])
plt.title("Forward Difference (normal scale)")
plt.xlabel("dx")
plt.ylabel("error")
plt.savefig(saved_graphs_dir + "normal-scale-forward_difference.png")
plt.show()

plt.plot(dx_range, backward_difference_error["1st Order"])
plt.plot(dx_range, backward_difference_error["2nd Order"])
plt.plot(dx_range, backward_difference_error["3rd Order"])
plt.legend(["1st Order", "2nd Order", "3rd Order"])
plt.title("Backward Difference (normal scale)")
plt.xlabel("dx")
plt.ylabel("error")
plt.savefig(saved_graphs_dir + "normal-scale-backward_difference.png")
plt.show()

plt.plot(dx_range, central_difference_error["2nd Order"])
plt.plot(dx_range, central_difference_error["4th Order"])
plt.legend(["2nd Order", "4th Order"])
plt.title("Central Difference (normal scale)")
plt.xlabel("dx")
plt.ylabel("error")
plt.savefig(saved_graphs_dir + "normal-scale-central_difference.png")
plt.show()

plt.plot(dx_range, forward_difference_error["2nd Order"])
plt.plot(dx_range, backward_difference_error["2nd Order"])
plt.plot(dx_range, central_difference_error["2nd Order"])
plt.legend(["forward", "backward", "central"])
plt.title("Forward vs Backward Vs Central (2nd Order- Normal Scale)")
plt.xlabel("dx")
plt.ylabel("error")
plt.savefig(saved_graphs_dir + "normal-scale-second_order.png")
plt.show()

plt.plot(dx_range, forward_difference_error["1st Order"])
plt.plot(dx_range, forward_difference_error["2nd Order"])
plt.plot(dx_range, forward_difference_error["3rd Order"])
plt.plot(dx_range, backward_difference_error["1st Order"])
plt.plot(dx_range, backward_difference_error["2nd Order"])
plt.plot(dx_range, backward_difference_error["3rd Order"])
plt.plot(dx_range, central_difference_error["2nd Order"])
plt.plot(dx_range, central_difference_error["4th Order"])
plt.legend(["Forward-1st Order", "Forward-2nd Order", "Forward-3rd Order", "Backward-1st Order",
	"Backward-2nd Order", "Backward-3rd Order", "Central-2nd Order", "Central-4th Order"])
plt.title("Forward vs Central vs Backward (Normal Scale)")
plt.xlabel("dx")
plt.ylabel("error")
plt.savefig(saved_graphs_dir + "normal-scale-all.png")
plt.show()

############################################
# Plot Error vs dx graphs in log-log scaling
############################################
plt.plot(np.log2(dx_range), np.log2(forward_difference_error["1st Order"]))
plt.plot(np.log2(dx_range), np.log2(forward_difference_error["2nd Order"]))
plt.plot(np.log2(dx_range), np.log2(forward_difference_error["3rd Order"]))
plt.legend(["1st Order", "2nd Order", "3rd Order"])
plt.title("Forward Difference (log-log scale)")
plt.xlabel("log(dx)")
plt.ylabel("log(error)")
plt.savefig(saved_graphs_dir + "log-scale-forward_difference.png")
plt.show()

plt.plot(np.log2(dx_range), np.log2(backward_difference_error["1st Order"]))
plt.plot(np.log2(dx_range), np.log2(backward_difference_error["2nd Order"]))
plt.plot(np.log2(dx_range), np.log2(backward_difference_error["3rd Order"]))
plt.legend(["1st Order", "2nd Order", "3rd Order"])
plt.title("Backward Difference (log-log scale)")
plt.xlabel("log(dx)")
plt.ylabel("log(error)")
plt.savefig(saved_graphs_dir + "log-scale-backward_difference.png")
plt.show()

plt.plot(np.log2(dx_range), np.log2(central_difference_error["2nd Order"]))
plt.plot(np.log2(dx_range), np.log2(central_difference_error["4th Order"]))
plt.legend(["2nd Order", "4th Order"])
plt.title("Central Difference (log-log scale)")
plt.xlabel("log(dx)")
plt.ylabel("log(error)")
plt.savefig(saved_graphs_dir + "log-scale-central_difference.png")
plt.show()

plt.plot(np.log2(dx_range), np.log2(forward_difference_error["2nd Order"]))
plt.plot(np.log2(dx_range), np.log2(backward_difference_error["2nd Order"]))
plt.plot(np.log2(dx_range), np.log2(central_difference_error["2nd Order"]))
plt.legend(["forward", "backward", "central"])
plt.title("Forward vs Backward Vs Central (2nd Order- log-log scale)")
plt.xlabel("log(dx)")
plt.ylabel("log(error)")
plt.savefig(saved_graphs_dir + "log-scale-second_order.png")
plt.show()

plt.plot(np.log2(dx_range), np.log2(forward_difference_error["1st Order"]))
plt.plot(np.log2(dx_range), np.log2(forward_difference_error["2nd Order"]))
plt.plot(np.log2(dx_range), np.log2(forward_difference_error["3rd Order"]))
plt.plot(np.log2(dx_range), np.log2(backward_difference_error["1st Order"]))
plt.plot(np.log2(dx_range), np.log2(backward_difference_error["2nd Order"]))
plt.plot(np.log2(dx_range), np.log2(backward_difference_error["3rd Order"]))
plt.plot(np.log2(dx_range), np.log2(central_difference_error["2nd Order"]))
plt.plot(np.log2(dx_range), np.log2(central_difference_error["4th Order"]))
plt.legend(["Forward-1st Order", "Forward-2nd Order", "Forward-3rd Order", "Backward-1st Order",
	"Backward-2nd Order", "Backward-3rd Order", "Central-2nd Order", "Central-4th Order"])
plt.title("Forward vs Central vs Backward (Log Scale)")
plt.xlabel("log(dx)")
plt.ylabel("log(error)")
plt.savefig(saved_graphs_dir + "log-scale-all.png")
plt.show()


#! Python3
# What is the 10 001st prime number?
# https://projecteuler.net/problem=7

# 1st approach: Using python's sympy module. Pretty fast.
#import sympy
#goal = 10001 # trying to find goal'th prime number
#i = j = 0
#while i < goal:
#    j += 1
#    if sympy.isprime(j):
#        i += 1
#        print(i)
#
#print(i-1, 'th prime number is ', j)

# 2nd aproach (takes about ?): Using sqrt from math module and own writen isprime function: Takes about 2 sec. Instead of checking till n, we can check till √n because a larger factor of n must be a multiple of smaller factor that has been already checked. And also algorith is further optimezed to just chech every 6th number TODO ??????

def isprime(number):
    from math import sqrt
    # returns true if number is prime number
    assign = True
    for i in range (2,int(sqrt(number))+1):
        if number % i == 0:
            assign = False
    return assign

pri_list = [2,3]
i = 1
while len(pri_list) < 10001:
    if isprime(i*6-1):
        pri_list.append(i*6-1)
    if isprime(i*6+1):
        pri_list.append(i*6+1)
    i += 1
print(len(pri_list), 'th prime number is ', pri_list[-1])

## 3rd aproach (takes about 2 seconds): Using just sqrt from math module and own writen isprime function: Takes about 2 sec. Instead of checking till n, we can check till √n because a larger factor of n must be a multiple of smaller factor that has been already checked.
#
#def isprime(number):
#    from math import sqrt
#    # returns true if number is prime number
#    assign = True
#    for i in range (2,int(sqrt(number))+1):
#        if number % i == 0:
#            assign = False
#    return assign
#goal = 10001 # trying to find goal'th prime number
#i = j = 0
#while i <= goal:
#    j += 1
#    if isprime(j):
#        i += 1
#
#print(i, 'th prime number is ', j)

## 4th approach: Naive brute force, using self written isprime function. It takes about 2 minutes to compute. That's why kept it at 100th for now.
#def isprime(number):
#    # returns true if number is prime number
#    assign = True
#    for i in range (2,number):
#        if number % i == 0:
#            assign = False
#    return assign
#
#goal = 100 # trying to find goal'th prime number
#i = j = 0
#while i <= goal:
#    j += 1
#    if isprime(j):
#        i += 1
#        print(i)
#
#print(i-1, 'th prime number is ', j)


import numpy as np
from numpy.random import uniform

from numerical.linear_equations import iterative
from numerical.linear_equations import iterative_sor, iterative_gauss_seidel

def demo_0():
    # Matt and Jane are selling fruit. Buyers can purchase small boxes of
    # apples and large boxes of apples. Matt sold 3 small boxes and 14 large
    # boxes for a total of 203€. Jane sold 11 small boxes and 11 large boxes
    # for a total of 220€. How much do the boxes cost? (the correct solution
    # is 7€ and 13€)

    # 3  x_0 + 14 x_1 = 203
    # 11 x_0 + 11 x_1 = 220
    A = np.array([
        [3, 14],
        [11, 11]])
    b = np.array([203, 220])

    result = iterative(A, b, x0=np.zeros(b.shape), acc=1e-7, omega=1)

    # let's take a look if the returned solution is correct
    print("Small box price: {:.2f}€".format(result['x'][0]))
    print("Large box price: {:.2f}€".format(result['x'][1]))

    # how many iterations were needed with different iterative methods?
    print("Number of iterations needed to find the solution:")
    print("Jacobi: {:d}".format(result['j_num_iter']))
    print("Gauss-Seidel: {:d}".format(result['gs_num_iter']))
    print("SOR: {:d}".format(result['sor_num_iter']))

def demo_1():
    A = np.array([
        [2, -1, 0, 0],
        [-1, 2, -1, 0],
        [0, -1, 2, -1],
        [0, 0, -1, 2]])
    b = np.array([1, 0, 0, 1])

    # let's compare number of iterations for the Gauss-Seidel
    # method and the SOR method
    _, gs_num_iter = iterative_gauss_seidel(
        A, b,
        x0=np.zeros(b.shape),
        acc=1e-7,
    )
    _, sor_num_iter = iterative_sor(
        A, b,
        x0=np.zeros(b.shape),
        acc=1e-7,
        omega=1
    )
    print("Number of iterations needed to find the solution:")
    print("Gauss-Seidel: {:d}".format(gs_num_iter))
    print("SOR with omega = 1: {:d}".format(sor_num_iter))

    # can we do better? -> random search
    fastest_convergence = (sor_num_iter, 1)

    for _ in range(100):
        omega = uniform(0, 2)
        _, sor_num_iter = iterative_sor(
            A, b,
            x0=np.zeros(b.shape),
            acc=1e-7,
            omega=omega
        )

        if sor_num_iter < fastest_convergence[0]:
            fastest_convergence = (sor_num_iter, omega)

    str_ = "SOR with omega = {:f}: {:d}"
    print(str_.format(fastest_convergence[1], fastest_convergence[0]))

if __name__ == '__main__':
    demo_0()
    print()
    demo_1()