'''
The numerical simulation calculates the approximation of the original curves from the SIR differential equations
using Euler's method
Code is adapted from:
https://scipython.com/book/chapter-8-scipy/additional-examples/the-sir-epidemic-model/
'''
import numpy as np #numpy is used
from scipy.integrate import odeint
import matplotlib.pyplot as plt

# Total population, N.
N = 1000000
# Initial number of infected and recovered individuals, I0 and R0.
I0, R0 = 305, 0
# Everyone else, S0, is susceptible to infection initially.
S0 = N - I0 - R0 #Susceptible is equal to the population less the infected and removed
# Contact rate, beta, and mean recovery rate, gamma, (in 1/days).
beta, gamma = 0.3552, 0.1 #Infectiousness and recovery rates
# A grid of time points (in days)
t = np.linspace(0, 200, 200) #length of time in days

# The SIR model differential equations.
def deriv(y, t, N, beta, gamma):
    S, I, R = y
    dSdt = -beta * S * I / N #derivative of susceptible equation
    dIdt = beta * S * I / N - gamma * I #derivative of infected equation
    dRdt = gamma * I #derivative of removed equation
    return dSdt, dIdt, dRdt #return the value of the derivatives

# Initial conditions vector
y0 = S0, I0, R0
# Integrate the SIR equations over the time grid, t.
ret = odeint(deriv, y0, t, args=(N, beta, gamma))
S, I, R = ret.T

# Plot the data on three separate curves for S(t), I(t) and R(t)
fig = plt.figure(facecolor='w')
ax = fig.add_subplot(111, axisbelow=True)
ax.plot(t, S/1000000, 'b', alpha=0.5, lw=2, label='Susceptible') #Plot the susceptible line as blue
ax.plot(t, I/1000000, 'r', alpha=0.5, lw=2, label='Infected') #plot the infected line as red
ax.plot(t, R/1000000, 'g', alpha=0.5, lw=2, label='Recovered with immunity') #plot the removed line as green
ax.set_xlabel('Time /days') #Set the label as time in days
ax.set_ylabel('Number (1000000s)') #population number is expressed in millions
ax.set_ylim(0,1.2) #the y-axis is limited to a maximum of 1.2
ax.yaxis.set_tick_params(length=0)
ax.xaxis.set_tick_params(length=0)
ax.grid(b=True, which='major', c='w', lw=2, ls='-')
plt.title('SIR model for the Ebola Outbreak in Liberia, 2014')
legend = ax.legend()
legend.get_frame().set_alpha(0.5)
for spine in ('top', 'right', 'bottom', 'left'):
    ax.spines[spine].set_visible(False)
plt.show() #Display the graph


import numpy as np

def calcAngle_py(v1,v2):

    v1 = v1/np.linalg.norm(v1)
    v2 = v2/np.linalg.norm(v2)

    return np.arccos(np.dot(v1,v2).clip(-1,1))

def calcRodriguesMtx_py(theta,k):
    """
    Get Rodrigues' rotation matrix for
    theta rad rotation (counter clockwise) w.r.t. a unit vector k

    see:
    https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula

    """

#     assert k.size == 3 and not np.allclose(k,np.zeros(3))

    # Ensure that k is a unit vector
    k = k/np.linalg.norm(k)

    # Cross-product matrix
    K = np.array([
            [    0,-k[2], k[1]],
            [ k[2],    0,-k[0]],
            [-k[1], k[0],   0]])

    return np.eye(3)+ K*np.sin(theta) + (K@K*(1-np.cos(theta)))

def calcAreaPoly_py(verts):
    """
    ref: http://geomalgorithms.com/a01-_area.html
    """

    L = verts.shape[0]
    assert L > 2, 'length of verts must be at least 3'

    # Get sequence of index
    idx1 = np.arange(L)
    idx2 = np.arange(1,L+1)
    idx2[-1] = 0

    # Get the unit normar vector
    n = np.cross(verts[1]-verts[0],verts[2]-verts[0])
    n /= np.linalg.norm(n)

    return abs(np.dot(np.sum(np.cross(verts[idx1],verts[idx2]),axis=0),n)/2)

    # assert L == 4, "Currently handle 4 vertices"

    # # Get the unit normar vector
    # n = np.cross(verts[1]-verts[0],verts[2]-verts[0])
    # n /= np.linalg.norm(n)

    # return abs(np.dot(np.cross(verts[2]-verts[0],verts[3]-verts[1]),n)/2)

def checkBlockage(plane1,plane2,planeB,nargout=1):
    """
    Check wheter plane1 and plane2 are blocked by planeB.

    plane1, plane2, planeB are simple planes defined in the
    rectplane class.

    """

    from owcsimpy.geoutils.cutils import calcArea3DPoly

    # Unpack the tuples
    _,ctrPoint1,_,_ = plane1
    _,ctrPoint2,_,_ = plane2
    normalVectB,ctrPointB,vertsB,areaB = planeB

    # Check intersection
    # see http://geomalgorithms.com/a05-_intersect-1.html
    u = ctrPoint2-ctrPoint1
    w = ctrPoint1-ctrPointB

    # print(np.dot(normalVectB,u))

    if np.allclose(np.dot(normalVectB,u),0):
        # Line segment and the planeB is parallel
        isBlocked = False
        intersectingPoint = None
    else:

        # Parametric value of the intersecting point
        ti = -np.dot(normalVectB,w)/np.dot(normalVectB,u)

        # The intersecting point
        intersectingPoint = ctrPoint1+ti*u

        # insert V[0] as V[n], see
        # http://geomalgorithms.com/a01-_area.html
        verts = np.append(vertsB,vertsB[0]).reshape(-1,3) # num of col is 3

        listTriangleVerts = [np.array([intersectingPoint.tolist(),verts[idx].tolist(),
                      verts[idx+1].tolist(),intersectingPoint.tolist()])
                     for idx in range(4)]

        totalarea = sum([calcArea3DPoly(3,triVerts[:,0],
            triVerts[:,1],triVerts[:,2],normalVectB)
        for triVerts in listTriangleVerts])

        if np.allclose(areaB,totalarea):
            isBlocked = True
        else:
            isBlocked = False

    if nargout == 1:
        return isBlocked
    elif nargout == 2:
        return isBlocked,intersectingPoint
    elif nargout == 3:
        return isBlocked,intersectingPoint,totalarea

def checkBlockageQHull(plane1,plane2,planeB,nargout=1):
    """
    Using ConvexHull.

    This is slower, but worth having as it's more intuitive.
    We can use this to have a third opinion.

    Check wheter plane1 and plane2 are blocked by planeB.

    plane1, plane2, planeB are simple planes defined in the
    rectplane class.

    """

    from scipy.spatial import Delaunay

    # Unpack the tuples
    _,ctrPoint1,_,_ = plane1
    _,ctrPoint2,_,_ = plane2
    normalVectB,ctrPointB,vertsB,areaB = planeB

    # Check intersection
    # see http://geomalgorithms.com/a05-_intersect-1.html
    u = ctrPoint2-ctrPoint1
    w = ctrPoint1-ctrPointB

    if np.allclose(np.dot(normalVectB,u),0):
        # Line segment and the planeB is parallel
        isBlocked = False
        intersectingPoint = None
    else:

        # Parametric value of the intersecting point
        ti = -np.dot(normalVectB,w)/np.dot(normalVectB,u)

        # The intersecting point
        intersectingPoint = ctrPoint1+ti*u

        verts = vertsB
        epsilon = 1e-3
        # for vert in vertsB:
        #     verts = np.append(verts,vert+normalVectB*epsilon)
        verts = np.append(verts,verts+normalVectB*epsilon)

        verts = verts.reshape(-1,3)

        delaunay = Delaunay(verts)
        isBlocked = delaunay.find_simplex(intersectingPoint)>=0

    if nargout == 1:
        return isBlocked
    elif nargout == 2:
        return isBlocked,intersectingPoint


import math
import numpy as np

class PurityFuncs:

    @staticmethod
    def entropy(labels):
        """ Calculate label entropy

        Arguments:
            labels {np.ndarray} -- A single dimensional array that we wish to calculate the entropy of
        Returns:
            {int} -- The entropy of the labels
        """

        total = len(labels)
        unique_labels, counts = np.unique(labels, return_counts=True)
        label_counts = dict(zip(unique_labels, counts))

        entropy = 0
        for label in unique_labels:
            prob = label_counts[label] / total
            entropy -= prob * math.log2(prob)
        return entropy

    @staticmethod
    def gini(labels):
        """ Calculates gini impurity of a given node's labels

        Arguments:
            labels {np.ndarray} -- A single dimensional array that we wish to calculate the entropy of
        Returns:
            {int} -- The gini impurity of the node
                For J classes, an impurity of (J-1)/J indicates an even distribution
                e.g. gini(['a','a','b','b','c','c') = 2 / 3 = 0.66667
        """

        total = len(labels)
        _, counts = np.unique(labels, return_counts=True)
        return 1 - np.sum(list(map(lambda p: (p / total) ** 2, counts)))


import pandas as pd
from sympy import Symbol, diff, factorial
from sympy.core.add import Add
from sympy.core.mul import Mul
from typing import Union

def n_diff(f: Union[Add, Mul], variable: Symbol, n: int):
	for _ in range(n):
		f = f.diff(variable)
	return f

def taylor(f: Union[Add, Mul], variable: Symbol, x0: float = 0.0, n: int = 10):
	fn = n_diff(f, variable, n)
	if n > 0:
		return ((fn.subs({variable: x0}) / factorial(n)) * (variable - x0) ** n) + taylor(f, variable, x0, n - 1)
	else:
		return (fn.subs({variable: x0}) / factorial(n)) * (variable - x0) ** n

def function(x: float):
	return x ** 2 - 4

def bisection(f: function, a: float, b: float, tol=0.001, iterations=100, history: pd.DataFrame = None):
	p = (a + b) / 2
	if history is None: history = pd.DataFrame(columns=["a", "b", "p", "f(p)"])

	if f(a) * f(b) > 0: Exception("No root exists between {} and {}".format(a, b))

	history = history.append({
		"a": a,
		"b": b,
		"p": p,
		"f(p)": f(p)
	}, ignore_index=True)

	if abs(f(p)) > tol and iterations > 0:
		if f(a) * f(p) > 0:
			a = p
		else:
			b = p
		return bisection(f, a, b, tol, iterations - 1, history)
	else:
		return p, history

def does_converges(g: Union[Add, Mul], variable: Symbol, x0):
	return abs(g.diff(variable).subs({variable: x0}).simplify()) < 1

def direct_iteration(f: Union[Add, Mul], variable: Symbol, variable_start: float = 0.0, iterations: int = 100):
	if does_converges(f, variable, variable_start):
		if iterations > 0:
			print(variable_start)
			variable_start = f.subs({variable: variable_start}).simplify()
			print(variable_start.evalf())
			print()
			return direct_iteration(f, variable, variable_start, iterations - 1)
		else:
			print(variable_start.evalf())
			return variable_start
	else:
		print("Does not converge at starting point given")

def newtons_method(f: Union[Add, Mul], variable: Symbol, x0: float = 0.0, tol: float = 0.001, iterations: int = 100):
	f_ = f.diff(variable)
	for i in range(iterations):
		numerator = f.subs({variable: x0})
		denominator = f_.subs({variable: x0})
		x1 = x0 - numerator / denominator
		print(x1)
		if abs(x0 - x1) > tol:
			return x1
		else:
			x0 = x1
	return x0


"""
...This file implements methods for matrix manipulation, including:

    1. matrix_power() ->
        divide-and-conquer strategies for
        finding a matrix exponential.
        This function makes use of Strassen's algorithm to optimize its runtime.

    2. matrix_multiply() ->
        an implementation of Strassen's O(n^log_2(7))
        algorithm for matrix faster matrix multiplication by substituting
        brute-force multiplication with strategic multiplications and additions.

    3. matrix_sum() ->
        Given two matrices, find their sum using brute-force methods.

    4. matrix_difference() ->
        Given two matrices A, B in order, find A - B using brute_force methods.

    5. matrix_get() ->
        A helper function that, given an n x n matrix such that n % 2 = 0
        and a position 0 <= x <= 3, returns the subset of the matrix.

    6. matrix_compile() ->
        A helper function that, given a larger matrix A and a smaller matrix B
        such that dim(A) = 2 * dim(B), saves the values in B into a
        specified subset of A.

    7. matrix_identity() ->
        A function that, given a dimension n, returns the (n x n) identity matrix.

    8. matrix_transpose() ->
        Given an (m x n) matrix, returns the (n x m) transpose of the matrix.

    IMPORTANT NOTE:
        It is usual to represent matrices as an array of arrays, wherein each sub-array
        represents either a column or row of the matrix.

        In this implementation, each sub-array is interpreted to be a column of the matrix.
        It's important that the caller create/implement matrices with similar
        semantics, or the calculations are not guaranteed to be correct.

    (c) Amittai J. Wekesa (github: @siavava), May 2021.
...
"""

from timeit import timeit
import numpy as np
from numpy.random import randint
from mpl_toolkits import mplot3d
from matplotlib import pyplot as plt
from math import log, log2, ceil

def matrix_power(matrix: list, exponential: int) -> list:
    """
    Compute the power of a matrix.
    """
    if exponential == 0:
        return matrix_identity(len(matrix))
    elif exponential == 1:
        return matrix
    else:
        half_power: list = matrix_power(matrix, exponential // 2)
        power: list = matrix_multiply(half_power, half_power)
        if (exponential % 2) == 0:
            return power
        else:
            return matrix_multiply(matrix, power)

def matrix_identity(n: int) -> list:
    """
    Generate the n-sized identity matrix.
    """
    if n == 0:
        return [0]
    identity: list = []
    for col in range(n):
        column: list = []
        for row in range(n):
            if col == row:
                column.append(1)
            else:
                column.append(0)
        identity.append(column)
    return identity

def matrix_generate(n: int, maximum=10**4) -> list:
    """
    Generate a random square matrix of given size
    Maximum size of element
    """
    if n == 0:
        return [0]
    m: list = []
    for col in range(n):
        column: list = []
        for row in range(n):
            column.append(randint(maximum))
        m.append(column)
    return m

def matrix_transpose(matrix: list) -> list:
    """
    Compute the transpose of a matrix
    """
    transpose: list = []
    for row in range(len(matrix[1])):
        column = []
        for col in range(len(matrix)):
            column.append(matrix[col][row])
        transpose.append(column)
    return transpose

def matrix_multiply(mat_a: list, mat_b: list):
    """
    Multiply rwo matrices.
    """
    n: int = len(mat_a)
    if n != len(mat_b):
        print("ERROR: Cannot multiply non-square matrices using Strassen. Stop.")
        return None
    elif n % 2 != 0:
        print("ERROR: Cannot work on matrices whose dimensions are not a factor of two.")
        print("An update will be coming soon!")
        return None

    if n > 2:
        mode = "matrix"
    else:
        mode = "nums"

    # sub-nums
    if mode == "nums":
        # Step 1: compute sums
        s1 = mat_b[1][0] - mat_b[1][1]
        s2 = mat_a[0][0] + mat_a[1][0]
        s3 = mat_a[0][1] + mat_a[1][1]
        s4 = mat_b[0][1] - mat_b[0][0]
        s5 = mat_a[0][0] + mat_a[1][1]
        s6 = mat_b[0][0] + mat_b[1][1]
        s7 = mat_a[1][0] - mat_a[1][1]
        s8 = mat_b[0][1] + mat_b[1][1]
        s9 = mat_a[0][0] - mat_a[0][1]
        s10 = mat_b[0][0] + mat_b[1][0]

        # Step 2: compute products
        p1 = mat_a[0][0] * s1
        p2 = mat_b[1][1] * s2
        p3 = mat_b[0][0] * s3
        p4 = mat_a[1][1] * s4
        p5 = s5 * s6
        p6 = s7 * s8
        p7 = s9 * s10

        # Step 3: compile results
        results = [[0, 0], [0, 0]]
        results[0][0] = p5 + p4 - p2 + p6
        results[1][0] = p1 + p2
        results[0][1] = p3 + p4
        results[1][1] = p5 + p1 - p3 - p7

        return results

    # sub-matrices
    elif mode == "matrix":
        # compute matrix sums
        s1 = matrix_difference(matrix_get(mat_b, 1), matrix_get(mat_b, 3))
        s2 = matrix_sum(matrix_get(mat_a, 0), matrix_get(mat_a, 1))
        s3 = matrix_sum(matrix_get(mat_a, 2), matrix_get(mat_a, 3))
        s4 = matrix_difference(matrix_get(mat_b, 2), matrix_get(mat_b, 0))
        s5 = matrix_sum(matrix_get(mat_a, 0), matrix_get(mat_a, 3))
        s6 = matrix_sum(matrix_get(mat_b, 0), matrix_get(mat_b, 3))
        s7 = matrix_difference(matrix_get(mat_a, 1), matrix_get(mat_a, 3))
        s8 = matrix_sum(matrix_get(mat_b, 2), matrix_get(mat_b, 3))
        s9 = matrix_difference(matrix_get(mat_a, 0), matrix_get(mat_a, 2))
        s10 = matrix_sum(matrix_get(mat_b, 0), matrix_get(mat_b, 1))

        # Step 2: compute matrix products
        p1 = matrix_multiply(matrix_get(mat_a, 0), s1)
        p2 = matrix_multiply(s2, matrix_get(mat_b, 3))
        p3 = matrix_multiply(s3, matrix_get(mat_b, 0))
        p4 = matrix_multiply(matrix_get(mat_a, 3), s4)
        p5 = matrix_multiply(s5, s6)
        p6 = matrix_multiply(s7, s8)
        p7 = matrix_multiply(s9, s10)

        # Step 3: compile results
        results = [[0] * n] * n
        results = matrix_compile(results, matrix_sum(matrix_sum(p5, p4), matrix_difference(p6, p2)), 0)
        results = matrix_compile(results, matrix_sum(p1, p2), 1)
        results = matrix_compile(results, matrix_sum(p3, p4), 2)
        results = matrix_compile(results, matrix_difference(matrix_sum(p5, p1), matrix_sum(p3, p7)), 3)

        # return compiled results
        return results

def matrix_sum(mat_a: list, mat_b: list):
    """
    Compute the sum of two compatible matrices.
    """
    n: int = len(mat_a)
    if n != len(mat_b):
        print("ERROR: cannot add incompatible matrices. Stop.")
        return None
    results = []
    for col in range(n):
        column = []
        for row in range(n):
            column.append(mat_a[col][row] + mat_b[col][row])
        results.append(column)
    return results

def matrix_difference(mat_a: list, mat_b: list):
    """
    Compute the difference of two matrices.
    """
    n: int = len(mat_a)
    if n != len(mat_b):
        print("ERROR: cannot subtract incompatible matrices. Stop.")
        return None
    results = []
    for col in range(n):
        column = []
        for row in range(n):
            column.append(mat_a[col][row] - mat_b[col][row])
        results.append(column)
    return results

def matrix_compile(main: list, sub: list, pos: int):
    """
    Compile sub-matrices into a main matrix.
    """
    n = len(sub)
    total = len(main)
    if len(main) != 2 * n:
        print("ERROR: Cannot merge matrices of incompatible dimension. Stop.")
        return None

    results = []

    if pos == 0:
        for col in range(total):
            column = []
            for row in range(total):
                if col in range(n) and row in range(n):
                    column.append(sub[col][row])
                else:
                    column.append(main[col][row])
            results.append(column)
        return results

    elif pos == 1:
        for col in range(total):
            column = []
            for row in range(total):
                if col in range(n, total) and row in range(n):
                    column.append(sub[col - n][row])
                else:
                    column.append(main[col][row])
            results.append(column)
        return results

    elif pos == 2:
        for col in range(total):
            column = []
            for row in range(total):
                if col in range(n) and row in range(n, total):
                    column.append(sub[col][row - n])
                else:
                    column.append(main[col][row])
            results.append(column)
        return results

    elif pos == 3:
        for col in range(total):
            column = []
            for row in range(total):
                if col in range(n, total) and row in range(n, total):
                    column.append(sub[col - n][row - n])
                else:
                    column.append(main[col][row])
            results.append(column)
        return results

def matrix_get(main: list, pos: int):
    """
    Get a subset of a matrix.
    """
    n = len(main)
    if n % 2 != 0:
        print("ERROR: Cannot split matrix with odd num of cols/rows. Stop.")
        return None

    mid = n // 2
    sub = []

    if pos == 0:
        for col in range(mid):
            column = []
            for row in range(mid):
                column.append(main[col][row])
            sub.append(column)
        return sub

    elif pos == 1:
        for col in range(mid):
            column = []
            for row in range(mid):
                column.append(main[mid + col][row])
            sub.append(column)
        return sub

    elif pos == 2:
        for col in range(mid):
            column = []
            for row in range(mid):
                column.append(main[col][mid + row])
            sub.append(column)
        return sub

    elif pos == 3:
        for col in range(mid):
            column = []
            for row in range(mid):
                column.append(main[mid + col][mid + row])
            sub.append(column)
        return sub

if __name__ == '__main__':
    a = [[1, 1], [1, 0]]
    b = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]
    d = [[1, 2, 0, 0], [3, 4, 0, 0], [0, 0, 5, 6], [0, 0, 7, 8]]
    c = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]

    h = [[1, 2], [3, 4]]
    print(matrix_multiply(d, d))
    print(matrix_power(d, 5))
    print(matrix_multiply(c, d))
    print(matrix_identity(4))
    print(matrix_transpose(b))
    print(matrix_power(b, 113))
    print(f"random matrix: {matrix_generate(8)}")

    # Add code to do the rest of this problem
    sizes: list = []
    runtimes: list = []
    data = []
    ns = [2 ** t for t in range(1, 7)]

    titles = {'family': 'serif', 'color': 'blue', 'size': 20}
    axes = {'family': 'serif', 'color': 'darkred', 'size': 15}
    power = 4
    for n in ns:
        print(f"Computing with size = {n}, power = {power}")
        matrix = matrix_generate(n, 10)
        sizes.append(n)
        data.append(timeit("matrix_power(matrix, 4)",
                                       number=10, globals=globals()))

    # Generate plots
    plt.subplot(121)
    plt.grid(True, which="both")
    plt.title("exponential = 4", fontdict=titles)
    plt.xlabel("size", fontdict=axes)
    plt.ylabel("runtime", fontdict=axes)
    plt.plot(sizes, data, "--ro")

    plt.subplot(122)
    plt.title("size = 2", fontdict=titles)
    plt.xlabel("exponential", fontdict=axes)
    plt.ylabel("runtime", fontdict=axes)
    size = 2
    maxInt = 10
    matrix = matrix_generate(size, maxInt)
    powers = []
    static_runtimes = []
    for power in range(600):
        print(f"Computing size = 2, power = {power}.")
        powers.append(power)
        static_runtimes.append(timeit("matrix_power(matrix, power)",
                                       number=10, globals=globals()))
    plt.plot(powers, static_runtimes, "--b+")
    plt.savefig('./output/matrix-powers.png', dpi=300, transparent=False)
    plt.show()
    print("FINISHED!")


#For Python 3
#System of Ordinary Differential Equations: SIR epidemiology model

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

"""
The system of ODEs that we will solve is (SIR model):
    S'(t) = -b*S(t)*I(t)/N
    I'(t) = b*S(t)*I(t)/N - g*I(t)
    R'(t) = g*I(t)
    Constants: b, N, g
Initial Conditions:
    S(0) = 999
    I(0) = 1
    R(0) = 0
Interval:
    0 <= t <= 200
"""

def SIR(y,t,b,N,g):
    S, I, R = y
    #S = number of susceptible
    #I = number of infections
    #R = number of recovered or deceased
    #N is the total population, b is the average number of contacts per person per time
    #g is transition (recovery) rate
    #https://en.wikipedia.org/wiki/Compartmental_models_in_epidemiology#The_SIR_model

    dydt = [-1*b*S*I/N,
            b*S*I/N - g*I,
            g*I]
    return dydt

def SIRD(y,t,b,N,g,m):
    S, I, R, D = y
    #S = number of susceptible
    #I = number of infections
    #R = number of recovered
    #D = number of deaths
    #N is the total population, b is the average number of contacts per person per time
    #g is recovery rate, m is the mortality rate
    #https://en.wikipedia.org/wiki/Compartmental_models_in_epidemiology#The_SIRD_model

    dydt = [-1*b*S*I/N,
            b*S*I/N - g*I - m*I,
            g*I,
            m*I]
    return dydt

def solve_SIRmodel(b,N,g,ti,tf,y0_SIR,solve_SIR=True,m=0,y0_SIRD=[]):
    """
    INPUT:
    b, N, g, ti, tf, y0_SIRD        #see SIR function
    OPTIONAL INPUT:
    ::boolean:: solve_SIR           #whether or not to solve SIR, default True; if False, solve for SIRD system
    m, y0_SIRD                      #see SIRD function
    """
    t = np.linspace(ti, tf, 5000)
    if solve_SIR == True:
        #https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.odeint.html
        #SIR model:
        sol_SIR = scipy.integrate.odeint(SIR, y0_SIR, t, args=(b, N, g))
        plt.title('SIR Model')
        plt.plot(t, sol_SIR[:,0], 'y', label='S(t)')
        plt.plot(t, sol_SIR[:,1], 'r', label='I(t)')
        plt.plot(t, sol_SIR[:,2], 'b', label='R(t)')
        plt.legend(loc='best')
        plt.xlabel('t')
        plt.grid()
        plt.show()
    else:
        #SIRD model (includes death rate):
        sol_SIRD = sol_SIR = scipy.integrate.odeint(SIRD, y0_SIRD, t, args=(b, N, g, m))
        plt.title('SIRD Model')
        plt.plot(t, sol_SIRD[:,0], 'y', label='S(t)')
        plt.plot(t, sol_SIRD[:,1], 'r', label='I(t)')
        plt.plot(t, sol_SIRD[:,2], 'b', label='R(t)')
        plt.plot(t, sol_SIRD[:,3], 'black', label='D(t)')
        plt.legend(loc='best')
        plt.xlabel('t')
        plt.grid()
        plt.show()

def main():
    b = 0.2
    N = 1000
    g = 0.1

    y0_SIR = [999,1,0] #Initial Conditions for SIR model

    m = 0.02
    y0_SIRD = [999,1,0,0] #Initial Conditions for SIRD model

    #set of b and g values to plot
    b_set = [0.2,0.5,0.12,0.4]
    g_set = [0.1,0.1,0.07,0.3]
    try:
        assert(len(b_set) == len(g_set)) #check if both lists are same length
    except AssertionError:
        print("ERROR: Lists not the same length")
        return False

    #Interval
    ti = 0
    tf = 200

    for i in range(0,len(b_set),1):
        solve_SIRmodel(b_set[i],N,g_set[i],ti,tf,y0_SIR)

    solve_SIRmodel(b,N,g,ti,tf,y0_SIR,False,m,y0_SIRD)

if __name__ == "__main__":
    main()


import numpy as np

def QRDecomposition(A):
	n = np.shape(A)[0] #pegando o tamanho das linhas de A
	m = np.shape(A)[1] #pegando o tamanho das colunas de A
	Q = np.zeros((n,m)) #declarando a matriz Q
	R = np.zeros((m,m)) #declarando a matriz R

	for j in range(0, m):
		A_column = A[:, j] #pegando as colunas da matriz A
		V = np.zeros(n) #declarando o vetor V
		V = A_column #V igual a coluna j de A
		for i in range (0, j):
			R[i,j] = Q[:,i].dot(A_column) #fazendo o calculo do R[i,j] = coluna i de Q * coluna j de A ( i != j)
			V -= (Q[:,i].dot(A_column))*Q[:,i] #fazendo o calculo de V
		R[j,j] = np.linalg.norm(V) # R[j,j] = norma da coluna j de A (i == j)
		Q[:,j] = V/np.linalg.norm(V) #normalizando V e atribuindo a coluna j de Q

	return Q, R

def QRDecompositionModificada(A):
	n = np.shape(A)[0] #pegando o tamanho das linhas de A
	m = np.shape(A)[1] #pegando o tamanho das colunas de A
	Q = np.zeros((n,m)) #declarando a matriz Q
	R = np.zeros((m,m)) #declarando a matriz R
	V = np.copy(A) #copiando A para V

	for j in range(0, m):
		for i in range (0, j):
			R[i,j] = Q[:,i].dot(V[:,j]) #fazendo o calculo do R[i,j] = coluna i de Q * coluna j de V ( i != j)
			V[:,j] -= (Q[:,i].dot(V[:,j]))*Q[:,i] #fazendo o calculo de V
		R[j,j] = np.linalg.norm(V[:,j]) # R[j,j] = norma da coluna j de V (i == j)
		Q[:,j] = V[:,j]/np.linalg.norm(V[:,j]) #normalizando V e atribuindo a coluna j de Q

	return Q, R

A = np.array([[1,2],[1,3],[-2,0]], dtype='double')
B = np.array([[3,1], [4,-1]], dtype = 'double')

print('Decomposicao QR classica\n')

(Q, R) = QRDecomposition(A)
print('{}\n\n{}\n\n{}'.format(Q, R, Q.dot(R)))
print('\n')
(Q, R) = QRDecomposition(B)
print('{}\n\n{}\n\n{}'.format(Q, R, Q.dot(R)))

print('\n\n')

A = np.array([[1,2],[1,3],[-2,0]], dtype='double')
B = np.array([[3,1], [4,-1]], dtype = 'double')

print('Decomposicao QR do Python\n')

(Q_python,R_python) = np.linalg.qr(A)
print('{}\n\n{}\n\n{}'.format(Q_python, R_python, Q_python.dot(R_python)))
print('\n')
(Q_python,R_python) = np.linalg.qr(B)
print('{}\n\n{}\n\n{}'.format(Q_python, R_python, Q_python.dot(R_python)))

print('\n\n')

A = np.array([[1,2],[1,3],[-2,0]], dtype='double')
B = np.array([[3,1], [4,-1]], dtype = 'double')

print('Decomposicao QR modificada\n')

(Q, R) = QRDecompositionModificada(A)
print('{}\n\n{}'.format(Q, R))
print('\n')
(Q, R) = QRDecomposition(B)
print('{}\n\n{}'.format(Q, R))


import numpy as np
from math import log2

def mean(X):
    """ Average value of a series of data """
    X = np.array(X)
    return (np.sum(X) / len(X))

def softmax(X):
    exps = np.exp(X - np.max(X))
    return exps / np.sum(exps)

def softmax_grad(softmax):
    # Reshape the 1-d softmax to 2-d so that np.dot will do the matrix multiplication
    s = softmax.reshape(-1,1)
    return np.diagflat(s) - np.dot(s, s.T)

def stablesoftmax(x):
    """Compute the softmax of vector x in a numerically stable way."""
    shiftx = x - np.max(x)
    exps = np.exp(shiftx)
    return exps / np.sum(exps)

def cross_entropy(y_hat, y):
    eps = 1e-15
    y = np.squeeze(y)
    c = sum((y * np.log(y_hat + eps)) + ((1 - y) * np.log(1 - y_hat + eps))) / -len(y)
    return c

def sigmoid(X):
    """ Sigmoid Function """
    return (1 / (1 + np.exp(-X)))

def relu(X):
    if (isinstance(X, np.ndarray)):
        return np.array([max(n, 0) for n in X])
    else:
        return max(X, 0)

def delta_cross_entropy(X,y):
    """
    X is the output from fully connected layer (num_examples x num_classes)
    y is labels (num_examples x 1)
    	Note that y is not one-hot encoded vector.
    	It can be computed as y.argmax(axis=1) from one-hot encoded vectors of labels if required.
    """
    m = y.shape[0]
    grad = softmax(X)
    grad[range(m),y] -= 1
    grad = grad/m
    return grad

def softmax_crossentropy_with_logits(logits, reference_answers):
    logits_for_answers = logits[np.arange(len(logits)), reference_answers]
    xentropy = - logits_for_answers + np.log(np.sum(np.exp(logits) + 1e-14, axis=-1))
    return xentropy

def grad_softmax_crossentropy_with_logits(logits, reference_answers):
    ones_for_answers = np.zeros_like(logits)
    ones_for_answers[np.arange(len(logits)), reference_answers] = 1

    softmax = np.exp(logits) / np.exp(logits).sum(axis=-1, keepdims=True)

    return (- ones_for_answers + softmax) / logits.shape[0]

if __name__ == '__main__':
    X = np.array([1, 2, 3, 4, 5])
    z = [1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0]
    print(sigmoid(X))
    print(softmax(z))
    y = np.array([0.1, 0.3])
    ypred = np.array([0.5, 0.6])

    print()
    print(cross_entropy(y, ypred))
    x2 = np.array([-1, -2.8, -0, 1.4, 1])
    print(relu(x2))


#ZADANIE 2
#Wykorzystaj metode eliminacji Gaussa do znalezienia
#rozwiazan ukladow rownan a) – d). Wykonaj obliczenia przy
#uzyciu udostepnionej funkcji i programu napisanego w Python.
#Przeanalizuj i opisz w sprawozdaniu dzialanie funkcji realizującej
#metodę eliminacji Gaussa.

import numpy as np

def gaussElimin(a,b):
    n = len(b)
    #procedura eliminacji
    for k in range(0, n-1):
        for i in range(k+1, n):
            if a[i, k] != 0.0:
                lam = a[i, k] / float(a[k, k])
                a[i, k+1:n] = a[i, k+1:n] - lam * a[k, k+1:n]
                b[i] = b[i] - lam * b[k]
    #procedura wyliczania rozwiazania i zapisu do macierzy b
    for k in range(n-1, -1, -1):
        b[k] = (b[k] - np.dot(a[k, k+1:n], b[k+1:n])) / float(a[k, k])
    return b

def testA():
    #macierze jako float
    a = np.array([[3.0, 1.0, -1.0], [1.0, 2.0, -1.0], [1.0, 1.0, 5.0]])
    b = np.array([181.05, 108.35, 142.55])
    print('Rozwiazanie(np.linalg.solve):')
    print(np.linalg.solve(a, b))
    Iout = gaussElimin(a,b)
    print('Rozwiazanie(gaussElimin):')
    print(Iout)

def testB():
    #macierze jako float
    a = np.array([[1.0, -1.0, 2.0], [3.0, 2.0, 1.0], [2.0, -3.0, -2.0]])
    b = np.array([5.0, 10.0, -10.0])
    print('Rozwiazanie(np.linalg.solve):')
    print(np.linalg.solve(a, b))
    Iout = gaussElimin(a,b)
    print('Rozwiazanie(gaussElimin):')
    print(Iout)

def testC():
    #macierze jako float
    a = np.array([[5.0, 1.0, 1.0, 1.0], [2.0, -1.0, -1.0, 1.0], [3.0, -1.0, 2.0, -2.0], [5.0, -4.0, 3.0, -2.0]])
    b = np.array([685.0, 165.0, 256.0, 361.0])
    print('Rozwiazanie(np.linalg.solve):')
    print(np.linalg.solve(a, b))
    Iout = gaussElimin(a,b)
    print('Rozwiazanie(gaussElimin):')
    print(Iout)

def testD():
    #macierze jako float
    a = np.array([[1.0, 3.0, 5.0], [2.0, 5.0, 1.0], [2.0, 3.0, 8.0]])
    b = np.array([10.0, 8.0, 3.0])
    print('Rozwiazanie(np.linalg.solve):')
    print(np.linalg.solve(a, b))
    Iout = gaussElimin(a,b)
    print('Rozwiazanie(gaussElimin):')
    print(Iout)

if __name__ == "__main__":
    print(30 * '-')
    print("Funkcja A")
    testA()
    print(30 * '-')

    print(30 * '-')
    print("Funkcja B")
    testB()
    print(30 * '-')

    print(30 * '-')
    print("Funkcja C")
    testC()
    print(30 * '-')

    print(30 * '-')
    print("Funkcja D")
    testD()
    print(30 * '-')

#Funkcja napisana samodzielnie w Python zwraca identyczne wyniki
#jak funkcja biblioteczna numpy.linalg.solve().
#Metoda eliminacji Gaussa wykorzystuje dwie fazy obliczen.
#Pierwsza faza jest faza eliminacji, ktora sprowadza problem obliczeniowy do Ux = c.
#Druga faza jest faza wstecznego podstawienia, ktora polega na podstawianiu
#uzyskanych rozwiazan za niewiadome.
#Wystepujaca w programie funkcja sklada sie z dwoch algorytmow.
#Pierwszy algorytm realizuje faze eliminacji.
#Drugi algorytm odpowiada za wyliczenie rozwiazan i ich zapis do macierzy b.


#gradient descent learning
#steven 29/02/2020 Initial
#11/03/2020  add plotSeekPoint to plot the seek process
#import random
import matplotlib.pyplot as plt
import numpy as np
from plotCommon import plotSub

def difference_derivative(f,x,h=0.00001): #one parameter fuction derivate
    return (f(x+h)-f(x))/h

def question_linearFuc(x):
    a = 6.5   #must a > 0
    b = 1.88 #the final optimum x
    c = -8.8  #the final minimum value
    return a*(x - b)**2 + c

def question_linearFucMin():
    """seek x, make f(x) = ax^2 + bx + c (a>0)  minimize
    this fuction is always as the simplest(one feature data) cost fuction in machine learning
    """

    #tolerance = 0.0000000001  #1.0e-15
    #max_iter = 10000
    iter = 0
    alpha = 0.001 #0.01 #learning rate
    seekList=[]

    x = -10  #random.random()
    stepInter = 0
    while True:
        gradient = difference_derivative(question_linearFuc,x,h=0.0001)

        x_next = x - alpha * gradient

        if iter % 20 == 0:
            print(iter," :",x,gradient,x_next,question_linearFuc(x),stepInter)
            seekList.append(x)

        #stepInter = (question_linearFuc(x) - question_linearFuc(x_next))**2
        stepInter = abs(question_linearFuc(x) - question_linearFuc(x_next))
        #if (stepInter < tolerance) or (iter > max_iter) :
        #if (stepInter - tolerance == 0) or (iter > max_iter) :
        #if (stepInter == 0) or (iter > max_iter) or (x - x_next == 0):
        if (stepInter == 0) or (x - x_next == 0):
            break

        x = x_next
        iter += 1

    print (iter,'result: x=',x, 'minValue:',question_linearFuc(x))

    plotSeekPoint(seekList)

def plotSeekPoint(seekList):
    _, ax = plt.subplots()

    x = np.linspace(-10,10,20)
    y = question_linearFuc(x)
    ax.plot(x,y) #plot function curve

    print(len(seekList))
    x = np.array(seekList)
    y = question_linearFuc(x)
    ax.scatter(x, y, s=10, color='r', alpha=0.75) #plot seekpoint in the seeking process
    plt.show()

def question_CurvePeakAndValley():
    """seek x, findout all curve's peak and valleys, that is where the derivative equal to 0"""
    def func(x):
        #return x**4
        #return  (x**3 + 3*x**2-6*x-8)/4
        return x**5 + 2*x**4 + 3*x**3 + 8*x**2 + 10*x

    x = np.linspace(-1.25,-0.65, 100)
    z0=[]
    for i, _ in enumerate(x):
        if i>=len(x)-1:
            break
        d1 = difference_derivative(func,x[i])
        d2 = difference_derivative(func,x[i+1])
        if d1*d2<=0:
            print('x = ',x[i])
            z0.append(x[i])
        #print('d1,d2,* = ',d1,d2,d1*d2)

    ax = plt.subplot(1,1,1)
    plotSub(x, func(x), ax, label='func',color='b')
    for k in z0:
        plt.vlines(k, np.min(func(x)), func(x[-1]),linestyles='dotted', color='r')
    #plt.axis('square')
    plt.legend()
    plt.grid()
    plt.show()

def main():
    #question_linearFucMin()
    question_CurvePeakAndValley()

if __name__ == '__main__':
    main()


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

def trapezoid(f, a, b, pieces, graph=False):
    """Find the integral of the function f between a and b using pieces trapezoids
    Args:
        f: function to integrate
        a: lower bound of integral
        b: upper bound of integral
        pieces: number of pieces to chop [a,b] into

    Returns:
        estimate of integral
    """
    integral = 0
    h = b - a
    if (graph):
        x = np.linspace(a,b,100)
        plt.plot(x,f(x),label="f(x)")
        ax = plt.subplot(111)
    #initialize the left function evaluation
    fa = f(a)
    for i in range(pieces):
        #evaluate the function at the left end of the piece
        fb = f(a+(i+1)*h/pieces)
        integral += 0.5*h/pieces*(fa + fb)
        if (graph):
            verts = [(a+i*h/pieces,0),(a+i*h/pieces,fa), (a+(i+1)*h/pieces,fb),(a+(i+1)*h/pieces,0)]
            poly = Polygon(verts, facecolor='0.8', edgecolor='k')
            ax.add_patch(poly)
        #now make the left function evaluation the right for the next step
        fa = fb

    if (graph):
        ax.set_xticks((a,b))
        ax.set_xticklabels(('a','b'))
        plt.xlabel("x")
        plt.ylabel("f(x)")
        if (pieces > 1):
            plt.title("Trapezoid Rule with " + str(pieces) + " pieces")
        else:
            plt.title("Trapezoid Rule with " + str(pieces) + " piece")
        plt.show()
    return integral

def trapezoid_rec(f, a, b, epsilon = 1.0e-6, old = -1.0e16, depth=0, graph=False):
    """Find the integral of the function f between a and b using pieces trapezoids
    Args:
        f: function to integrate
        a: lower bound of integral
        b: upper bound of integral
        old: keeps track of how much the estimate changes from recursion to level
        depth: how many levels of recursion do we have, python only allows so many

    Returns:
        estimate of integral
    """
    h = b - a
    #break interval into two pieces and do trapezoid on each
    if (graph) and (depth == 0):
        x = np.linspace(a,b,100)
        plt.plot(x,f(x),label="f(x)")

    new_estimate_left = 0.25*h*(f(a) + f(a+0.5*h))
    new_estimate_right = 0.25*h*(f(a+0.5*h) + f(b))

    #check to see if the sum of the two pieces is close to the old guess
    if (np.fabs(new_estimate_left + new_estimate_right - old) > epsilon) and (depth < 100):
        #if not, then call trapezoid_rec again on each half-interval
        integral = (trapezoid_rec(f,a,a+0.5*h,epsilon=epsilon, old=new_estimate_left,depth=depth+1,graph=graph) +
                    trapezoid_rec(f,a+0.5*h,b,epsilon=epsilon,old=new_estimate_right,depth=depth+1,graph=graph))

        return integral
    else:
        #halving the interval didn't change much so we can stop

        if (graph):
            ax = plt.subplot(111)
            verts = [(a,0),(a,f(a)), (a+0.5*h,f(a+0.5*h)),(a+0.5*h,0)]
            poly = Polygon(verts, facecolor='0.8', edgecolor='k')
            ax.add_patch(poly)
            verts = [(a+0.5*h,0),(a+0.5*h,f(a+0.5*h)), (b,f(b)),(b,0)]
            poly = Polygon(verts, facecolor='0.8', edgecolor='k')
            ax.add_patch(poly)

        if (graph) and (depth == 0):
            ax.set_xticks((a,b))
            ax.set_xticklabels(('a','b'))
            plt.xlabel("x")
            plt.ylabel("f(x)")
            plt.title("Trapezoid Rule with recursion")
            plt.show()
        return new_estimate_left + new_estimate_right

def quadratic_interp(a,f,x):
    """Compute at quadratic interpolant
    Args:
        a: array of the 3 points
        f: array of the value of f(a) at the 3 points
    Returns:
        The value of the linear interpolant at x
    """
    answer = (x-a[1])*(x-a[2])/(a[0]-a[1])/(a[0]-a[2])*f[0]
    answer += (x-a[0])*(x-a[2])/(a[1]-a[0])/(a[1]-a[2])*f[1]
    answer += (x-a[0])*(x-a[1])/(a[2]-a[0])/(a[2]-a[1])*f[2]
    return answer

def simpsons(f, a, b, pieces, graph=False):
    """Find the integral of the function f between a and b using Simpson's rule
    Args:
        f: function to integrate
        a: lower bound of integral
        b: upper bound of integral
        pieces: number of pieces to chop [a,b] into

    Returns:
        estimate of integral
    """
    integral = 0
    h = b - a
    one_sixth = 1.0/6.0
    if (graph):
        x = np.linspace(a,b,100)
        plt.plot(x,f(x),label="f(x)")
        ax = plt.subplot(111)

    #initialize the left function evaluation
    fa = f(a)
    for i in range(pieces):
        #evaluate the function at the left end of the piece
        fb = f(a+(i+1)*h/pieces)
        fmid = f(0.5*(a+(i+1)*h/pieces+ a+i*h/pieces))
        integral += one_sixth*h/pieces*(fa + 4*fmid + fb)
        if (graph):
            ix = np.arange(a+i*h/pieces, a+(i+1)*h/pieces, 0.001)
            iy = quadratic_interp(np.array([a+i*h/pieces,0.5*(a+(i+1)*h/pieces+ a+i*h/pieces),a+(i+1)*h/pieces]),
                                  np.array([fa,fmid,fb]),ix)
            verts = [(a+i*h/pieces,0)] + list(zip(ix,iy)) + [(a+(i+1)*h/pieces,0)]
            poly = plt.Polygon(verts, facecolor='0.8', edgecolor='k')
            ax.add_patch(poly)
        #now make the left function evaluation the right for the next step
        fa = fb

    if (graph):
        ax.set_xticks((a,b))
        ax.set_xticklabels(('a','b'))
        plt.xlabel("x")
        plt.ylabel("f(x)")
        plt.title("Simpsons Rule with " + str(pieces) + " pieces")
        plt.show()
    return integral

def RichardsonExtrapolation(fh, fhn, n, k):
    """Compute the Richardson extrapolation based on two approximations of order k
    where the finite difference parameter h is used in fh and h/n in fhn.
    Inputs:
    fh:  Approximation using h
    fhn: Approximation using h/n
    n:   divisor of h
    k:   original order of approximation

    Returns:
    Richardson estimate of order k+1"""
    n = decimal.Decimal(n)
    k = decimal.Decimal(k)
    numerator = decimal.Decimal(n**k * decimal.Decimal(fhn) - decimal.Decimal(fh))
    denominator = decimal.Decimal(n**k - decimal.Decimal(1.0))
    return float(numerator/denominator)

def Romberg(f, a, b, MaxLevels = 10, epsilon = 1.0e-6, PrintMatrix = False):
    """Compute the Romberg integral of f from a to b
    Inputs:
    f:  integrand function
    a: left edge of integral
    b: right edge of integral
    MaxLevels: Number of levels to take the integration to

    Returns:
    Romberg integral estimate"""

    estimate = np.zeros((MaxLevels,MaxLevels))

    estimate[0,0] = trapezoid(f,a,b,pieces=1)
    count = 1
    converged = 0
    while not(converged):
        estimate[count,0] = trapezoid(f,a,b,pieces=2**count)
        for extrap in range(count):
            estimate[count,1+extrap] = RichardsonExtrapolation(estimate[count-1,extrap],
                                                               estimate[count,extrap],2,2**(extrap+1))

        converged = np.fabs(estimate[count,count] - estimate[count-1,count-1]) < epsilon
        if (count == MaxLevels-1): converged = 1
        count += 1
    if (PrintMatrix):
        print(estimate[0:count,0:count])
    return estimate[count-1, count-1]

def RombergSimpson(f, a, b, MaxLevels = 10, epsilon = 1.0e-6, PrintMatrix = False):
    """Compute the Romberg integral of f from a to b
    Inputs:
    f:  integrand function
    a: left edge of integral
    b: right edge of integral
    MaxLevels: Number of levels to take the integration to

    Returns:
    Romberg integral estimate"""

    estimate = np.zeros((MaxLevels,MaxLevels))

    estimate[0,0] = simpsons(f,a,b,pieces=1)
    count = 1
    converged = 0
    while not(converged):
        estimate[count,0] = simpsons(f,a,b,pieces=2**count)
        for extrap in range(count):
            estimate[count,1+extrap] = RichardsonExtrapolation(estimate[count-1,extrap],
                                                               estimate[count,extrap],n=2,k=2+2.0**(extrap+1))

        converged = np.fabs(estimate[count,count] - estimate[count-1,count-1]) < epsilon
        if (count == MaxLevels-1): converged = 1
        count += 1
    if (PrintMatrix):
        print(estimate[0:count,0:count])
    return estimate[count-1, count-1]


# 面试题9：斐波那契数列题目一：写一个函数，输入n，
# 求斐波那契（Fibonacci）数列的第n项。斐波那契数列的定义如下：
import numpy as np
def fib(n):
    if n <= 2:
        return 1

    return fib(n-1)+fib(n-2)

memo = {}
def fib_memo(n):
    if n <= 2:
        return 1
    if n in memo:
        return memo[n]

    memo[n] = fib(n-1)+fib(n-2)

    return memo[n]

def fib_iter(n):

    a,b = 1,1

    if n <= 2:
        return 1

    for _ in range(n-2):
        a,b = b,a+b

    return b

def matrixMulti(A,B):
    length = len(A)
    width = len(B[0])
    res = [[0]*width for _ in range(length)]
    print(res)
    for i in range(length):
        for j in range(width):
            for k in range(len(B)):
                # print("i:{},j:{},k:{}".format(i,j,k))
                # print("".format(i,k,A[i][k],k,j,B[k][j]))
                # print("res[{}][{}]:{}".format(i,j,res[i][j]))
                res[i][j] += A[i][k] * B[k][j]
                # print("A[{}][{}]:{}*B[{}][{}]:{}=res[{}][{}]:{}".format(i,k,A[i][k],k,j,B[k][j],i,j,res[i][j]))
    return res

def fibLog(matrix,n):

    res = [[1,0],[0,1]]

    tmp = matrix

    while n > 0:

        if n & 1 != 0:
            res = np.dot(res,tmp)

        tmp = np.dot(tmp,tmp)

        n >>= 1
    return res

    # if n == 0:
    #     return [[1,0],[0,1]]

    # if n == 1:
    #     return matrix

    # res = fibLog(matrix,n>>1)

    # res = np.dot(res,res)

    # if n & 0x1 == 1:
    #     res = np.dot(matrix,res)

    return res

if __name__ == "__main__":
    # print(fib(5))
    # print(fib(10))
    # print(fib_memo(10))
    # print(fib_iter(10))

    # A = [[1,2,3],[4,5,6]]
    # B = [[1,2],[3,4],[5,6]]
    # print(matrixMulti(A,B))
    # print(matrixMulti([[1,1],[1,0]],[[1,1],[1,0]]))
    print(fibLog([[1,1],[1,0]],3))
    # print(np.dot([[1,1],[1,0]],[[1,1],[1,0]]))


#########################################################################
#            SIMPSON'S RULES
#
# This module (based on Mike Zingale's code)
#    1) calculates the integral of f(x) = sin (pi x) over the
#     interval [0,1], using N = 3,7,15 and 31 slabs/intervals, for odd Ns.
#    2) Plots the absolute error vs delta = (b-a)/N.
#
#    (Marina von Steinkirch, spring 2013)
#########################################################################

import math
import numpy
import pylab

""" Functions """

def func(x):
    """ function to integrate"""
    fx = numpy.sin(math.pi*x)
    return fx

def exact_int(a,b):
    """ analytic value of the integral """
    I = (numpy.cos(math.pi*a) - numpy.cos(math.pi*b ))/math.pi
    return I

def simpson_int_odd(a,b,f,N):
    """ calculates remaining odd slabs """
    xedge = numpy.linspace(a,b,N+1)
    delta = xedge[1] - xedge[0]
    Is_odd = (delta/12.0)*(-f(xedge[N-2]) + 8.0*f(xedge[N-1]) + 5.0*f(xedge[N]))
    return Is_odd

def simpson_int(a,b,f,N):
    """"do a Simpson's integration by breaking up the domain [a,b] into N, considering N is even."""
    # MZ -- you were passing in the wrong N, so the xedge was wrong
    xedge = numpy.linspace(a,b,N+1)
    delta = xedge[1] - xedge[0]
    Is = 0.0
    n = 0
    # MZ: with the proper N, this loop executed too many times
    while n < N-1:
        Is += (delta/3.0)*(f(xedge[n]) + 4.0*f(xedge[n+1]) + f(xedge[n+2]))
        n += 2

    return Is

def printing(slab, I, Is, ea):
    """ print output """
    print "\nNumber of slabs to be Integrated: ", slab
    print "Simpson's Integral value: ", Is
    print "Analytic Integral value: ", I
    print "Absolute error: ", ea
    return 0

""" Variables """
CONST_A = 0.0
CONST_B = 1.0
CONST_N_SLABS = [3, 7, 15, 31]

ea_array = []
delta_array = []

""" Main Function"""
""" Since the intervals are odd, we need to do extra calculations in the edges """
for i in range (len(CONST_N_SLABS)):
    slab = CONST_N_SLABS[i]

    if not slab%2 == 0:
        Is_odd = simpson_int_odd(CONST_A, CONST_B, func, slab)
        edge = slab - 1

    else:
        edge = slab
        Is_odd = 0.0

    # MZ: you don't call this with edge -- your linspace needs to know the
    # correct N (not N-1) to get the right xedge values, so you call
    # with slab
    Is = simpson_int(CONST_A, CONST_B, func, slab)
    Is = Is + Is_odd
    I = exact_int(CONST_A, CONST_B)

    ea =   abs(Is - I)
    delta = abs(CONST_B -CONST_A)/slab

    printing(slab, I, Is, ea)

    ea_array.append(ea)
    delta_array.append(delta)

""" Plotting ea vs delta"""
pylab.loglog(delta_array, ea_array,  'bo')
pylab.xlabel('$\delta$ = (b-a)/N')
pylab.ylabel('Absolute Error')
pylab.grid(True)
pylab.savefig("simp.png")

""" Plotting ea vs N"""
pylab.clf()
pylab.cla()
pylab.loglog(CONST_N_SLABS, ea_array,  'go')
pylab.xlabel('N')
pylab.ylabel('Absolute Error')
pylab.grid(True)
pylab.savefig("simp2.png")

print "\n\nDone!"


## Import the packages
import numpy as np
from scipy import stats

## Define 2 random distributions
#Sample Size
N = 10
#Gaussian distributed data with mean = 2 and var = 1
a = np.random.randn(N) + 2
#Gaussian distributed data with with mean = 0 and var = 1
b = np.random.randn(N)

## Calculate the Standard Deviation
#Calculate the variance to get the standard deviation

#For unbiased max likelihood estimate we have to divide the var by N-1, and therefore the parameter ddof = 1
var_a = a.var(ddof=1)
var_b = b.var(ddof=1)

#std deviation
s = np.sqrt((var_a + var_b)/2)
s

## Calculate the t-statistics
t = (a.mean() - b.mean())/(s*np.sqrt(2/N))

## Compare with the critical t-value
#Degrees of freedom
df = 2*N - 2

#p-value after comparison with the t
p = 1 - stats.t.cdf(t,df=df)

print("t = " + str(t))
print("p = " + str(2*p))
### You can see that after comparing the t statistic with the critical t value (computed internally) we get a good p value of 0.0005 and thus we reject the null hypothesis and thus it proves that the mean of the two distributions are different and statistically significant.

## Cross Checking with the internal scipy function
t2, p2 = stats.ttest_ind(a,b)
print("t = " + str(t2))
print("p = " + str(p2))

print("ACRL test!")
a =  np.array([0.92,0.93,0.93,0.96,0.90])
b =  np.array([0.87,0.00,0.76,0.91,0.87])
c =  np.array([0.87,0.76,0.91,0.87])
b[1] = c.mean()
t2, p2 = stats.ttest_ind(a,b)
print("improve: " + str(a.mean()-b.mean()))
print("t = " + str(t2))
print("p = " + str(p2))

print("Time test!")
a =  np.array([102, 105, 98])
b =  np.array([1.08, 1.25, 1.15, 1.24, 1.15])
t2, p2 = stats.ttest_ind(a,b)
print("improve: " + str(a.mean()/b.mean()))
print("t = " + str(t2))
print("p = " + str(p2))


import numpy as np
import pandas as pd

def asset_over_time_apy(P:int, r:float, n:int, t:float) -> pd.DataFrame:
    """
    Compute the ending value of some investment after a certain amount of time with compound interest.

    Parameters
    ---------
    P: int
        Initial investment
    r: float
        Annual Interest Rate
    n: int
        Number of compounding periods per year
    t: int
        Number of years

    Returns
    ---------
    amount: pd.DataFrame
        Final amount

    Examples
    ---------

    >>> # Suppose we invest $5,000 into an investment that compounds at 6% annually.
    >>> # Calculate the ending value of this investment after 10 years:
    >>> P, r, n, t = 5000, .06, 1, 10
    >>> df = asset_over_time_apy(P=5000, r=.06, n=1, t=10)
    >>> # Expected df[-1] = 8954.238483

    """

    df = pd.DataFrame(data={'time': np.linspace(start=1, stop=t, num=t)})
    df['value']= df.applymap(lambda t: P*(pow((1+r/n), n*t)))

    return df

if __name__ == "__main__":
   """
   Run for testing only and store example
   """

   """
   https://www.statology.org/compound-interest-in-python/
   Example 1: Compound Interest Formula with ANNUAL Compounding
   Suppose we invest $5,000 into an investment that compounds at 6% annually.
   Calculate the ending value of this investment after 10 years:
   """

   df = asset_over_time_apy(P=5000, r=.06, n=1, t=10)
   print(df)
   # Expected df[-1] = 8954.238483

   """
   Example 2: Compound Interest Formula with MONTHLY Compounding
   Suppose we invest $1,000 into an investment that compounds at 6% annually
   and is compounded on a monthly basis (12 times per year).
   Calculate the ending value of this investment after 5 years:
   """

   df = asset_over_time_apy(P=1000, r=.06, n=12, t=2)
   print(df)


import numpy as np

def cross_product_matrix(in_vector):
    """
    Return the cross product matrix of a 3d vector

    For more information, see:
    https://en.wikipedia.org/wiki/Cross_product#Conversion_to_matrix_multiplication

    Parameters
    ----------
    in_vector : 3 x 1 numpy matrix
        Input vector
    Returns
    -------
    out_mat = 3 x 3 numpy array
        The cross product matrix of in_vector

    """
    # Alternate implementation for curiosity
    # I = np.identity(3)
    # out_mat = np.zeros((3,3))
    # for row in I:
    #     cross = np.cross(in_vector,row)
    #     outer = np.outer(cross,row)
    #     out_mat += outer
    out_mat = np.array([[0.,-in_vector[2],in_vector[1]],
                        [in_vector[2],0,-in_vector[0]],
                        [-in_vector[1],in_vector[0],0.]])
    return out_mat

def rotation_matrix(axis_vector, angle, degrees = True):
    """
    Return the rotation matrix corresponding to a rotation axis and angle

    For more information, see:
    https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle

    Parameters
    ----------
    axis_vector : 3 x 1 numpy array
        A unit vector of the axis of rotation
    angle : float
        Angle of rotation in degrees unless otherwise specified
    degrees : bool (optional)
        Choose between units of degrees of radians. Default True so degrees
    Returns
    -------
    rot_mat : 3 x 3 numpy array
        Rotation matrix

    """
    ang = angle
    if degrees:
        ang = np.radians(ang)
    # the matrix the sum of 3 terms
    cos_id = np.cos(ang) * np.identity(3)
    sin_cross = np.sin(ang) * cross_product_matrix(axis_vector)
    cos_tens = (1-np.cos(ang)) * np.outer(axis_vector,axis_vector)
    # total
    rot_mat = cos_id + sin_cross + cos_tens

    return rot_mat


# mean

# The mean tool computes the arithmetic mean along the specified axis.
# import numpy
# my_array = numpy.array([ [1, 2], [3, 4] ])
# print numpy.mean(my_array, axis = 0)        #Output : [ 2.  3.]
# print numpy.mean(my_array, axis = 1)        #Output : [ 1.5  3.5]
# print numpy.mean(my_array, axis = None)     #Output : 2.5
# print numpy.mean(my_array)                  #Output : 2.5

# var
# The var tool computes the arithmetic variance along the specified axis.
# import numpy
# my_array = numpy.array([ [1, 2], [3, 4] ])
# print numpy.var(my_array, axis = 0)         #Output : [ 1.  1.]
# print numpy.var(my_array, axis = 1)         #Output : [ 0.25  0.25]
# print numpy.var(my_array, axis = None)      #Output : 1.25
# print numpy.var(my_array)                   #Output : 1.25

# By default, the axis is None. Therefore, it computes the variance of the flattened array.

# std
# The std tool computes the arithmetic standard deviation along the specified axis.
# import numpy
# my_array = numpy.array([ [1, 2], [3, 4] ])
# print numpy.std(my_array, axis = 0)         #Output : [ 1.  1.]
# print numpy.std(my_array, axis = 1)         #Output : [ 0.5  0.5]
# print numpy.std(my_array, axis = None)      #Output : 1.11803398875
# print numpy.std(my_array)                   #Output : 1.11803398875

# By default, the axis is None. Therefore, it computes the standard deviation of the flattened array.

# Problem Statement:

# You are given a 2-D array of size X.
# Your task is to find:
# The mean along axis
# The var along axis
# The std along axis

import numpy as np

np.set_printoptions(legacy="1.13")
row, cols = map(int, raw_input().split())

l = []
for i in range(row):
    arr = list(map(float, raw_input().split()))
    l.append(arr)
l = np.array(l).reshape(row, cols)
print(np.array(np.mean(l, axis=1)))
print(np.array(np.var(l, axis=0)))
a = np.array(np.std(l, axis=None))
print(a)

# Input :

# 2 2
# 1 2
# 3 4

# Output :

# [ 1.5  3.5]
# [ 1.  1.]
# 1.11803398875


# the code is the demonstration of the linear regression
# TODO - Implement LARS and proximal gradient descent methods for LASSO

# import the required python libraries
import numpy as np
import numpy.linalg as la
from sklearn import preprocessing

class LinearRegression:

    def fit(self, key, X_train, Y_train, regularization_param=0, num_epochs=10):

        # fit the algorithm provided the training data
        params = [];
        if(key == 'ls'):
            params = self.fit_ls(X_train, Y_train);
        elif(key == 'rr'):
            params = self.fit_rr(X_train, Y_train, regularization_param);
        elif(key == 'lasso'):
            params = self.fit_lasso(X_train, Y_train, regularization_param, num_epochs);
        else:
            raise ValueError('Please provide the correct value of the ley argument');

        return params;

    def fit_ls(self, X_train, Y_train):

        print('------------Using Linear regression with Least squares model-------------------');
        print('Model training data shape:', X_train.shape);
        print('Model training data shape:', Y_train.shape);

        # determine the paramters of the model using linear regression least sqaures
        dd_correlation = np.matmul(np.transpose(X_train), X_train);
        dl_correlation = np.matmul(np.transpose(X_train), Y_train);
        params = np.matmul(la.inv(dd_correlation), dl_correlation);

        return params;

    def fit_rr(self, X_train, Y_train, lambda_param):

        print('----------Using Linear regression with L2 regularization----------------------');
        print('Model training data shape:', X_train.shape);
        print('Model training data shape:', Y_train.shape);

        # determine the paramters of the model using linear regression with L2 regularization
        # note that a close form solution exists for this type of regression.

        tmp = np.matmul(np.transpose(X_train), X_train);
        dd_correlation = np.add(tmp, lambda_param* np.identity(tmp.shape[0]));

        dl_correlation = np.matmul(np.transpose(X_train), Y_train);
        params = np.matmul(la.inv(dd_correlation), dl_correlation);

        return params;

    def fit_lasso(self, X_train, Y_train, lambda_param, num_epochs):

        print('----------Using Linear regression with L1 regularization----------------------');
        print('Model training data shape:', X_train.shape);
        print('Model training data shape:', Y_train.shape);

        learning_rate = 0.00001;
        print('Learning rate:', learning_rate);

        # determine the paramters of the model using linear regression with L1 regularization
        # note that a close form solution does not exists for this type of regression.
        # use the vanilla gradient descent method. Using the vanilla gradient descent gives only
        # approximate sparse solutions(not completely zero)

        X_train = preprocessing.scale(X_train);
        Y_train = preprocessing.scale(Y_train);

        params = np.ones(X_train.shape[1]);

        num_samples = X_train.shape[1];

        for i in range(num_epochs):

            # compuet the loss on the prediction
            rss_loss = Y_train - np.matmul(X_train, params);
            mse = la.norm(rss_loss, ord=2) + lambda_param * la.norm(params, ord=1);
            # print mse;
            # compute the gradient
            # notice the subgradient used in the computation of the gradient of L1 norm
            gradient = (-np.dot(np.transpose(X_train), rss_loss) + \
                        lambda_param * np.sign(params))/num_samples;

            # update the parameters
            params = params - learning_rate * gradient;

        # print params;
        return params;


import numpy as np

""" Determinant of upper triangle or diagnal matrix is product of diagnal element """
a = np.array([(2, 3, 4), (0, 2, 5), (0, 0, 2)])
# array([[2, 3, 4],
#        [0, 2, 5],
#        [0, 0, 2]])

print(np.linalg.det(a))
# 7.999999999999998

""" Determinant of row exhanged matrix will change the sign """
a = np.array([(2, 3, 4), (0, 0, 2), (0, 2, 5)])
print(np.linalg.det(a))
# - 7.999999999999998

""" Determinant of identiy matrix is 1 """
a = np.array([(1, 0, 0), (0, 1, 0), (0, 0, 1)])
print(np.linalg.det(a))
# 1

""" Determinant of zero row contain matrix is 0 """
a = np.array([(2, 3, 4), (0, 0, 0), (9, 2, 5)])
print(np.linalg.det(a))
# 0

""" Determinant of singular (dependent column) matrix is 0 """
a = np.array([( 2, 6,  4 ),
              ( 6, 18, 12),
              ( 9, 27, 5 )])
print(np.linalg.det(a))
# 0

""" Determinant of non-singular (independent column) matrix is non zero """
a = np.array([(2, 6, 4), (6, 7, 12), (9, 9, 5)])
print(np.linalg.det(a))
# 285.99999999999994


# Question 4 Lab 03
# AB Satyaprakash (180123062)

# imports ----------------------------------------------------------------------
from math import factorial
import numpy as np
import sympy as sp
# ------------------------------------------------------------------------------
# functions --------------------------------------------------------------------

def forwardDiff(fArray):
    sz = len(fArray)
    fdArray = [fArray]
    for i in range(1, sz):
        temp = []
        for j in range(sz-i):
            temp.append(fdArray[i-1][j+1]-fdArray[i-1][j])
        fdArray.append(temp)
    return fdArray

def newtonFDPoly(fArray, xArray):
    x0, x1 = xArray[0], xArray[1]
    h = x1-x0
    u = np.array([1/h, -x0/h])
    fdArray = forwardDiff(fArray)
    sz = len(fArray)
    px = np.array([0])

    for i in range(sz):
        term = np.array([1])
        for j in range(i):
            term = np.polymul(term, np.polyadd(u, np.array([-j])))
            term = term/(j+1)
        term = term*fdArray[i][0]
        px = np.polyadd(px, term)
    return px

# ------------------------------------------------------------------------------
# g(x) = sin(x)/(x^2). Calculate g(0.25)
# (a) By direct interpolation
X = [0.1, 0.2, 0.3, 0.4, 0.5]
G = [9.9833, 4.9667, 3.2836, 2.4339, 1.9177]

# Construct a degree 3 polynomial to appproximate g(0.25)
px = newtonFDPoly(G[:4], X[:4])
print('Thus, the approximation of g(0.25) using direct interpolation = P(0.25) = {}'.format(np.polyval(px, 0.25)))
# ((x−0.1)(x−0.2)(x−0.3)(x−0.4)*Δ_4(f0))/4!(0.1)^4 is used to find the error!
Δ4_f0 = forwardDiff(G)[4][0]
x = sp.Symbol('x')
errorExpression = ((x-0.1)*(x-0.2)*(x-0.3)*(x-0.4)*Δ4_f0)/(factorial(4)*(0.1**4))
errorValue = abs(errorExpression.subs(x, 0.25))
print('The error term is at x = 0.25 is', errorValue)

# (b) By first tabulating xg(x) and then forward difference interpolating in that table
for i in range(5):
    G[i] *= X[i]
px = newtonFDPoly(G[:4], X[:4])
print('\n\nThus, the approximation of g(0.25) using interpolation on xg(x) table = P(0.25) = {}'.format(
    np.polyval(px, 0.25)*4))
Δ4_f0 = forwardDiff(G)[4][0]
x = sp.Symbol('x')
errorExpression = ((x-0.1)*(x-0.2)*(x-0.3)*(x-0.4)*Δ4_f0)/(factorial(4)*(0.1**4))
errorValue = abs(errorExpression.subs(x, 0.25)*4)
print('The error term is at x = 0.25 is', errorValue)

# (c) Explain the difference between the results in (i) and (ii) respectively.
print('\nSince the differences in (i) are oscillating and are not decreasing fast, the resulting error in interpolation would be large.')
print('However, the differences in (ii) tend to become smaller in magnitude, we expect more accurate results in this case.')

# Question 4 ends --------------------------------------------------------------

Semester 6/MA 322 (Scientific Computing Theory)/Labs/Lab 3/Code/q4.py


# 数值微分求导数， 数值微分就是用数值方法近似的求解函数的导数的过程
import numpy as np
import matplotlib.pyplot as plt

# f = function, x为input

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

# example 1: f = 0.01x^2 + 0.1x
def function1(x):
    return 0.01 * (x ** 2) + 0.1 * x

# 此函数用于利用数值微分的值作为斜率，重新画出与对应函数的斜线
def tangent_line(f, x):
    d = numerical_diff(f, x) # 计算导数
    print(d)
    y = f(x) - d*x # f(x)与直线的截距
    return lambda t: d*t + y # 类似返回一个直线函数 y = k * x + b

def function(x):
    return 0.02 * x + 0.1

# define 二元函数f(x0, x1) = x0^2 + x1^2
def function2(x):
    return np.sum(x ** 2)

# 当x0 = 3， x1 = 4时， 求funciton2对x0的偏微分
def function2_x0(x0):
    return x0**2 + 4.0 ** 2.0
print('当x0 = 3， x1 = 4时， 求funciton2对x0的偏微分: ', numerical_diff(function2_x0, 3.0))

# 当x0 = 3， x1 = 4时， 求funciton2对x1的偏微分
def function2_x1(x1):
    return 3.0 ** 2.0 + x1 ** 2
print('当x0 = 3， x1 = 4时， 求funciton2对x1的偏微分: ', numerical_diff(function2_x1, 4.0))

x = np.arange(0.0, 20.0, 0.1)
y = function1(x)
tf = tangent_line(function1, 5)
y1 = tf(x)
y2 = function(x)
plt.xlabel("x")
plt.ylabel("f(x)")
plt.plot(x, y)
plt.plot(x, y1)
plt.plot(x, y2)
plt.show()

# 如果通过我们自己计算导数f(x)的导数为0.02*x + 0.1
# 当x in 5： 0.02 * 5 + 0.1 = 0.2
# 当x in 10：0.02 * 10 + 0.1 = 0.3
# compare to this result, discover similarly
print('x in 5的导数：', numerical_diff(function1, 5))

print('x in 10的导数: ', numerical_diff(function1, 10))


#Program to find the eigen values and eigen vectors.
#Developed by: SRIJITH R
#RegisterNumber: 21004191
import numpy as np
A = np.array([[2,-3,0],[2,-5,0],[0,0,3]])
values,vectors=np.linalg.eig(A)
print("Eigen values are {} and Eigen Vectors are {}".format(values,vectors))


import numpy as np

#TODO add doc + add multiple root finding
def newton(function, derivative, x0, max_depth=10,  eps=1e-10):
    x=x0
    i=0
    while i < max_depth:
        if (derivative(x)==0):
            print("Singularity at x={}".format(x))
        if np.abs(function(x))<=eps:
            return x
        x=x-function(x)/derivative(x)
        i=i+1
    return x

# Find the third root of 27
f = lambda x : 1/3*np.log(27)-np.log(x)
Df = lambda x : -1/x
x = newton(f, Df, 2, 5)
print("The third root of 27 is : {}".format(x))

# Find the the golden ratio
f = lambda x : x**2-x-1
Df = lambda x : 2*x -1
x = newton(f, Df, 4, 6)
print("The golden ratio is : {}".format(x))

# Find the the super golden ratio
f = lambda x : x**3-x**2-1
Df = lambda x : 3*x**2-2*x
x = newton(f, Df, 4, 6)
print("The super golden ratio is : {}".format(x))

# Unprecise function choice
# Find the the value of pi
f = lambda x : np.cos(x)
Df = lambda x : -np.sin(x)
x = newton(f, Df, 1.55, 1000, 1e-10)
print("The value of pi is : {}".format(2*x))


import pandas as pd
import numpy as np
import scipy.stats as st
import statsmodels.api as sm
import matplotlib.pyplot as plt
import seaborn as sns

#Calculating covariance and correlation coeffecient using hand
ad = pd.DataFrame({'watched':[5,4,4,6,8], 'bought':[8,9,10,13,15]})
print(ad)

print(ad.describe())

print(ad.corr())

_ = sns.lmplot('watched', 'bought', data=ad)
plt.show()

# cross-product deviations: multiply the deviations of one variable by the corresponding deviations of a second variable
# for above examples Cross Product Deviations
watched_diff, bought_diff = [], []
for index, row in ad.iterrows():
    watched_diff.append(5.4-row['watched'])
    bought_diff.append(11-row['bought'])

print(watched_diff, bought_diff)

Cross_Product_Deviations = sum(np.multiply(watched_diff, bought_diff))
print(Cross_Product_Deviations)

# Covariance (we got it from cross product deviations)
# covariance = Cross_Product_Deviations/(N-1)
Covariance = Cross_Product_Deviations/(5-1)
print(Covariance)

# now
watched_diff_standarized = np.array(watched_diff)/(ad['watched'].std())
bought_diff_standarized = np.array(bought_diff)/(ad['bought'].std())

Cross_Product_Deviatiions_standarized = sum(np.multiply(watched_diff_standarized, bought_diff_standarized))
print(Cross_Product_Deviatiions_standarized)

# R, correlation coefficient is standarized covariance

Covariance_standarized = Cross_Product_Deviatiions_standarized/(5-1)
print(Covariance_standarized)

# also , we can get it by R = Covariance/(s1*s2)
print(Covariance/(ad['watched'].std() * ad['bought'].std()))

# Correlations

data = pd.read_csv('/home/atrides/Desktop/R/statistics_with_Python/06_Correlation/Data_Files/Exam Anxiety.dat', sep='\t')

print(data.head())

data.set_index('Code', inplace=True, drop=True)

# pearson
print(data.corr())

# kendall
print(data.corr(method='kendall'))

# using scipy.stats, only does one pair of variables at a time

print(st.pearsonr(data['Revise'], data['Exam']))

print(st.spearmanr(data['Revise'], data['Exam']))

print(st.kendalltau(data['Revise'], data['Exam']))

print(st.pearsonr(data['Anxiety'], data['Exam']))

# Note:
#
# use pearson r for parametric
#
# use spearman rho for non-parametric
#
# use tendall tau for non-parametric and small datasets

# seeing the relation with lmplot
_ = sns.lmplot('Revise', 'Exam', data=data)
plt.show()

# R^2 : measure of the amount of variability in one variable that is shared by the other

print((data.corr())**2)

import numpy,import scipy,import statsmodels


# -*- coding: utf-8 -*-
"""
Created on Mon Sep 16 10:03:34 2019

@author: BSWOOD9321
"""

import numpy as np
import matplotlib.pyplot as plt

x = np.arange(-2*np.pi,2*np.pi,np.pi/20)
sin = np.sin(x)
derivative = np.cos(x)
fdiff = ((np.sin(x+np.pi/20))-np.sin(x))/((x+np.pi/20)-x)
plt.plot(x,fdiff,label='Forward Difference Function')
plt.plot(x,derivative,'r--',label='Derivative[Cos(x)]')
plt.title("Forward Difference Equation and Cosine function")
plt.xlabel("x")
plt.legend(loc=(1.04,0))
plt.show()

grad = np.gradient(sin,np.pi/20)
plt.plot(x,grad,label='Gradient command')
plt.plot(x,fdiff,'r--',label='Forward Difference Function')
plt.plot(x,derivative,'b-',label='Derivative[Cos(x)]')
plt.xlabel("x")
plt.title("ForDiff and Cos(x) and Gradient command")
plt.legend(loc=(1.04,0))
plt.show()

Errorf = np.absolute(fdiff-derivative)/derivative

Errorg = np.absolute(grad-derivative)/derivative

plt.plot(x,Errorf,label='Forward Differential Error')
plt.legend(loc=(1.04,0))
plt.show()
plt.plot(x,Errorg,label='Gradient Error')
plt.legend(loc=(1.04,0))
plt.show()

print("So there is a small difference between the two different ways to find the derivative.")
print("The errors of each, while at similar locations (it looks to be at the points where the derivative=0), have different values.")
print("This is probably due to how the functions calculate the values really close to zero.")

x1 = np.arange(-2*np.pi,2*np.pi,np.pi/20)
sin1 = np.sin(x1+.25)
derivative1 = np.cos(x1+.25)
fdiff1 = ((np.sin(x1+np.pi/20+.25))-np.sin(x1+.25))/((x1+np.pi/20)-x1)
plt.plot(x1,fdiff1,label='Forward Difference Function')
plt.plot(x1,derivative1,'r--',label='Derivative[Cos(x)]')
plt.title("Challenge Plot 1")
plt.xlabel("x")
plt.legend(loc=(1.04,0))
plt.show()

grad1 = np.gradient(sin1,np.pi/20)
plt.plot(x1,grad1,label='Gradient command')
plt.plot(x1,fdiff1,'r--',label='Forward Difference Function')
plt.plot(x1,derivative1,'b-',label='Derivative[Cos(x)]')
plt.xlabel("x")
plt.title("Challenge Plot 2")
plt.legend(loc=(1.04,0))
plt.show()

Errorf1 = np.absolute(fdiff1-derivative1)/derivative1

Errorg1 = np.absolute(grad1-derivative1)/derivative1

plt.plot(x1,Errorf1,label='Forward Differential Error')
plt.legend(loc=(1.04,0))
plt.title("Challenge Error 1")
plt.show()
plt.plot(x1,Errorg1,label='Gradient Error')
plt.legend(loc=(1.04,0))
plt.title("Challenge Error 2")
plt.show()

x2 = np.arange(1,10,.20)
func = (x2**3)/3
derivative2 = x2**2
fdiff2 = (((x2+.20)**3)/3-((x2**3)/3)/.20)
plt.plot(x2,fdiff2,label='Forward Difference Function')
plt.plot(x2,derivative2,'r--',label='Derivative[x^2]')
plt.title("Bonus Plot 1")
plt.xlabel("x")
plt.legend(loc=(1.04,0))
plt.show()

grad2 = np.gradient(func,np.pi/20)
plt.plot(x2,grad2,label='Gradient command')
plt.plot(x2,fdiff2,'r--',label='Forward Difference Function')
plt.plot(x2,derivative2,'b-',label='Derivative[Cos(x)]')
plt.xlabel("x")
plt.title("Bonus Plot 2")
plt.legend(loc=(1.04,0))
plt.show()

Errorf2 = np.absolute(fdiff2-derivative2)/derivative2

Errorg2 = np.absolute(grad2-derivative2)/derivative2

plt.plot(x2,Errorf2,label='Forward Differential Error')
plt.legend(loc=(1.04,0))
plt.title("Bonus Error 1")
plt.show()
plt.plot(x2,Errorg2,label='Gradient Error')
plt.legend(loc=(1.04,0))
plt.title("Bonus Error 2")
plt.show()


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

def plot_beta_distribution(a,b,name,points=100):
    '''
    This function will plot the beta distribution give the parameters a & b
    '''
    #Getting the normalixation constant
    constant=gamma(a+b)/(gamma(a)*gamma(b))

    #Sampling the points uniformly
    delta_x=1.0/points
    x=np.arange(0,1+delta_x,delta_x)

    #Now calculating the pdf on these points
    beta=constant*(x**(a-1))*((1-x)**(b-1))

    #Plotting the pdf
    plt.plot(x,beta)
    plt.xlabel('mu (u)')
    plt.ylabel('probability_density')
    plt.title('Probability Density Function : P(u|a,b)')
    #plt.show()
    plt.savefig(name)
    plt.clf()

def iterative_learning(prior_a,prior_b,toss_sample,plot_control=10):
    #Getting the metadata
    N=toss_sample.shape[0]

    #Plotting the initial prior distribution
    plot_beta_distribution(prior_a,prior_b,name='prior.png')
    #Now going iteratively and plotting the beta distribution
    for i in range(N):
        #Calculating the new posteror based on the current evidence of the data
        posterior_a = prior_a+toss_sample[i]
        posterior_b = prior_b+(1-toss_sample[i])

        #Plotting the new distribution
        if(i%plot_control==0):
            plot_beta_distribution(posterior_a,posterior_b,name=str(i)+'.png')

        #Updating the current posterir as prior for next iteration
        prior_a=posterior_a
        prior_b=posterior_b

def one_shot_learning(prior_a,prior_b,toss_sample):
    '''
    This function will give the final posterior distribution by seeing the
    whole data at once.
    '''
    #Getting the metadata
    N=toss_sample.shape[0]
    correction_m=np.sum(toss_sample)
    correction_l=N-correction_m

    #Getting the posteror parameters of the distribution
    posterior_a = prior_a+correction_m
    posterior_b = prior_b+correction_l

    #Plotting the final distribution
    plot_beta_distribution(posterior_a,posterior_b,name='one_shot.png')

if __name__=='__main__':
    ############ CONTROL VARIABLES ##############
    dataset_size=150
    #Parameters for the prior
    a=2
    b=int((0.6/0.4)*a)

    ########### MAIN CODE ######################
    #Sampling the dastaset randomly
    toss_sample=np.random.randint(0,2,size=dataset_size)

    #Plotting the distibution iteratively
    print "Plotting the Iterative solution"
    iterative_learning(a,b,toss_sample,plot_control=1)

    #Plotting the distribution for the one-shot learning
    print "Printing the One-Shot Solution"
    one_shot_learning(a,b,toss_sample)


"""
@file:          least_square.py
@description:   answer to problems in exercises 6.5
@author:        Hiu-kong Dan
@date:          June 1, 2021
"""

from sympy import Matrix

def problem26():
    A = Matrix([[0, .7, 1],
                [-.7,0,.7],
                [-1,-.7,0],
                [-.7,-1,-.7],
                [0,-.7,-1],
                [.7,0,-.7],
                [1,.7,0],
                [.7,1,.7],
                [0,-.7,1],
                [.7,0,-.7],
                [-1,.7,0],
                [.7,-1,.7],
                [0,.7,-1],
                [-.7,0,.7],
                [1,-.7,0],
                [-.7,1,-.7]])
    b = Matrix([.7,
                0,
                -.7,
                -1,
                -.7,
                0,
                .7,
                1,
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                0])

    assert A.shape == (16,3)
    assert b.shape == (16,1)

    M = (A.T * A).col_insert(4, A.T * b)

    assert M.shape == (3, 4)

    x = Matrix.rref(M)[0].col(3)

    print("Using row reduction:")
    print(x)
    print("-"*20)

    # using another mathod
    print("Using inverse matrix:")
    x = (A.T * A).inv() * A.T * b
    print(x)

if __name__ == "__main__":
    problem26()


import sympy
from sympy import *
import random

def choose_modulus(k,l,m):
	p1 = find_prime(k,l)
	p2 = find_prime(k,l)
	#while not (log(p1,2) + m < log(p2, 2)):
	#	p1 = find_prime(k,l)
	#	p2 = find_prime(k,l)
	#	print(p1, p2)
	#print(log(p1,2)+m < log(p2,2))
	print("prime 1: ", p1)
	print("prime 2: ", p2)
	return p1,p2

def choose_encryption_key_old(m):
	e = random.randrange(2,m)
	while not gcd(e, totient(m)) == 1:
		e = random.randrange(2,m)
	return e

def choose_encryption_key(p1,p2):
	m = p1*p2
	e = random.randrange(2, p1*p2)
	while not gcd(e, (p1-1)*(p2-1)) == 1:
		e = random.randrange(2, p1*p2)
	return e

def compute_decryption_key(e, p1, p2):
	# d = e inverse mod phi(m) m = p1*p2
	#phi(m) = (p1-1)*(p2-1)
	d = inv_mod(e, (p1-1)*(p2-1))
	return d

def RSA_encrypt(P, e, m):
	return power_mod(P,e,m)

def RSA_decrypt(C, d, m):
	return power_mod(C,d,m)

def RSA_crack(C, e, m):
	return power_mod(C, inv_mod(e,totient(m)), m)

def power_mod(a, b, m):
	#replace with better version for large numbers
	#return ((a%m)**(b%m))%m
	return pow(a,b,m)

def string_to_int(s):
	return int.from_bytes(s.encode(),'big')

def int_to_string(n):
	return n.to_bytes((n.bit_length()+7)//8,'big').decode()

def find_prime(k, l):
	x = random.randrange(2**k+2, 2**l-1)
	while (not isprime(x)):
		x = random.randrange(2**k+2, 2**l-1)
	return x

def mod_inv_old(a,m):
	#replace with euclid's algorithm - will be using large numbers
	for i in range(0,m):
		if (i*a)%m == 1:
			return i
	return -1

def xgcd(b, n):
    """ Return g, x0, y0
        such that x0*b + y0*n = g
        and g is the gcd of (b,n)"""
    x0, x1, y0, y1 = 1, 0, 0, 1
    while n != 0:
        q, b, n = b // n, n, b % n
        x0, x1 = x1, x0 - q * x1
        y0, y1 = y1, y0 - q * y1
    return b, x0, y0

def inv_mod(b, n):
    """ Return the modular inverse of b mod n
     or None if gcd(b,n) > 1 """
    g, x, _ = xgcd(b, n)
    if g == 1:
        return x % n

def unit_test1(kv, lv, dv, textv):
	(p1, p2) = choose_modulus(kv, lv, dv)
	m = p1 * p2
	e = choose_encryption_key(p1, p2)
	d = compute_decryption_key(e, p1, p2)
	plaintext = textv
	P = string_to_int(plaintext)
	print("Plaintext in bits = ", P)
	C = RSA_encrypt(P, e, m)
	print("Plaintext: ", plaintext)
	print("Text Len: ", 8*len(plaintext), "bits")
	print("Modulus len: ",  int(log(m)/log(2)), "bits")
	print("Ciphertext in bit format = ", C)
	D = RSA_decrypt(C, d, m)
	print("D (Decrypted text in bit format) = ", D)
	decoded_text = int_to_string(D)
	print("Decoded Text: ", decoded_text, "\n\n--------------\n")

def unit_test2(kv, lv, dv, textv):
	(p1, p2) = choose_modulus(kv, lv, dv)
	m = p1 * p2
	e = choose_encryption_key(p1, p2)
	d = compute_decryption_key(e, p1, p2)
	plaintext = textv
	P = string_to_int(plaintext)
	print("Plaintext in bits = ", P)
	C = RSA_encrypt(P, e, m)
	print("Plaintext: ", plaintext)
	print("Text Len: ", 8*len(plaintext), "bits")
	print("Modulus len: ",  int(log(m)/log(2)), "bits")
	print("C (Ciphertext in bit format) = ", C)
	D = RSA_crack(C, e, m)
	print("D (decrypted text in bit format) = ", D)
	decoded_text = int_to_string(D)
	print("Decoded Text: ", decoded_text, "\n\n--------------\n")

unit_test1(10, 12, 1, "Hi")
unit_test1(200,201,50,"I've got the best RSA. Tremendous!")
unit_test2(10, 12, 5, "Hi")


#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time     : 2018/12/06 12:41
# @Author   : Iydon
# @File     : 9.3.py

import numpy as np
import scipy as sp

def gersgorin_circle(A, numpy):
    r"""
    |z-a_{ii}|<=\sum_{j!=i}|a_{ij}|
    """
    shape = A.shape
    if shape[0] != shape[-1]:
        raise Exception("Dimensions do not match.")
    result = [[] for i in range(shape[0])]
    for i in range(shape[0]):
        summation = numpy.sum(numpy.abs(A[i,])) - numpy.abs(A[i,i])
        result[i] = [A[i,i], summation]
    return result

def power_method(A, numpy, x0, judge=None, eps=1e-6, max_loop=64, disply=False, p=0):
    r"""
    Numpy
    B = A - pI.
    """
    if judge==None: judge = lambda x,y: numpy.linalg.norm(x-y, float("inf"))
    A_    = A.copy() - p*numpy.eye(A.__len__())
    count = 0
    mu    = numpy.max(numpy.abs(x0))
    x0    = x0 / mu
    last  = x0
    while count<max_loop:
        count += 1
        uv = numpy.matmul(A_, x0)
        mu = numpy.max(numpy.abs(uv))
        x0 = uv / mu
        if disply:
            print("%%%dd: %%s"%(len(str(max_loop)))%(count, mu))
        if judge(last,x0)<eps:
            break
        last = x0
    return mu + p

def inverse_power_method(A, scipy, x0, judge=None, eps=1e-6, max_loop=64, disply=False, p=0):
    r"""
    Scipy
    pass
    """
    from scipy import linalg
    if judge==None:
        norm  = lambda x: scipy.linalg.norm(x, float("inf"))
        judge = lambda x,y: abs(norm(x-y))
    A_    = A.copy() - p*scipy.eye(len(A))
    LU    = linalg.lu_factor(A_)
    count = 0
    mu    = max(abs(x0))
    x0    = x0 / mu
    last  = x0
    while count<max_loop:
        count += 1
        uv = linalg.lu_solve(LU, x0)
        mu = max(abs(x0))[0]
        x0 = uv / mu
        if disply:
            print("%%%dd: %%s"%(len(str(max_loop)))%(count, p+1/mu))
        if judge(last,x0)<eps:
            break
        last = x0
    return p + 1/mu

A  = np.matrix([[1,1,1],[1,1,0],[1,0,1]])
x0 = np.matrix([-1,0,1]).T
result = power_method(A, np, x0=x0, judge=None, eps=1e-6, max_loop=64, disply=True, p=0)
print(result)

result = inverse_power_method(A, np, x0=x0, judge=None, eps=1e-6, max_loop=64, disply=True, p=0)
print(result)

print(np.max(np.linalg.eigvals(A)))
print(np.min(np.linalg.eigvals(A)))

A  = np.matrix([[2,1,1],[1,2,1],[1,1,2]])
x0 = np.matrix([1,-1,2]).T
result = power_method(A, np, x0=x0, judge=None, eps=1e-6, max_loop=64, disply=True, p=0)
print(result)
result = inverse_power_method(A, np, x0=x0, judge=None, eps=1e-6, max_loop=64, disply=True, p=0)
print(result)

print(np.max(np.linalg.eigvals(A)))
print(np.min(np.linalg.eigvals(A)))

print("\n"*10)

A = np.matrix([[0,0,2,4],[1/2,0,0,0],[0,1/4,0,0],[0,0,1/8,0]])
x0 = np.matrix([[1],[1],[1],[1]])
result = power_method(A, np, x0=x0, judge=None, eps=1e-6, max_loop=128, disply=True, p=0)
print(result)
print(np.max(np.linalg.eigvals(A)))

print("\n"*10)

M = 11
a = 3/4
A = np.diag([(1+2*a) for i in range(M-1)]) - np.diag([a for i in range(M-2)],1) - np.diag([a for i in range(M-2)],-1)
x0 = np.ones((M-1,1))
result = inverse_power_method(A, np, x0=x0, judge=None, eps=1e-6, max_loop=128, disply=True, p=0)

print(result)
print(np.min(np.linalg.eigvals(A)))

print("\n"*10)

M = 11
a = 3/4
A = np.diag([(1+a) for i in range(M-1)]) - np.diag([-a/2 for i in range(M-2)],1) - np.diag([-a/2 for i in range(M-2)],-1)
B = np.diag([(1+a) for i in range(M-1)]) - np.diag([a/2 for i in range(M-2)],1) - np.diag([a/2 for i in range(M-2)],-1)
C = np.matmul( np.linalg.inv(A), B )
x0 = np.ones((M-1,1))
result = power_method(C, np, x0=x0, judge=None, eps=1e-6, max_loop=1024, disply=True, p=0)

print(result)
print(np.max(np.linalg.eigvals(C)))


import numpy as np
def minkowski_distances(vector_x, vector_z, p=2):
    """ Calculates the Minkowski distance for a choosen value p between two vectors.

    -----------
    Parameters:

    vector_x / vector_z : numpy array
            Both are expected to have the same size and conntain float values

    p : int or +/- np.inf
            The value to  raise and then take the root in the minkowski distance formula

    Using +/- np.inf for representing a infinity, they are preprocessed to their max/min representation.
    Be careful since very large values of p may take a some time to process, even though we use a vectorized approach
    """
    if p == np.inf:
        return max(np.abs(vector_x - vector_z))
    elif p == - np.inf:
        return min(np.abs(vector_x - vector_z))

    if not isinstance(p, int):
        raise TypeError("p is {type(p)}, not a integer!")
    elif p <= 0:
        raise ValueError("p must be a non negative, non zero integer!")

    return np.sum(np.abs(vector_x-vector_z)**p) ** (1/p)

def hamming_distance(vector_x, vector_z):
    """ Returns the Hamming distance (amount of different elements) between two vectors
    We divide by the length of the vector to follow sklean's convention

    Usefull as a distance metric for categorical data
    ----------
    Parameters
    vector_x / vector_z : numpy array or pd.Series
    """

    # If you want, you can check scipy's hamming distance, but it still gives
    #   different results to sklearn
    #from scipy.spatial.distance import hamming
    #return hamming(vector_x, vector_z)
    return np.sum(~(vector_x == vector_z)) / len(vector_x)


# IMPORTS
import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt
import matplotlib.style as style
from IPython.core.display import HTML

# PLOTTING CONFIG
%matplotlib inline
style.use('fivethirtyeight')
plt.rcParams["figure.figsize"] = (14, 7)
HTML("""
<style>
.output_png {
    display: table-cell;
    text-align: center;
    vertical-align: center;
}
</style>
""")
plt.figure(dpi=100)

# PDF P = .2
plt.scatter(np.arange(21),
            (stats.binom.pmf(np.arange(21), p=.2, n=20)),
            alpha=0.75,
            s=100
       )
plt.plot(np.arange(21),
         (stats.binom.pmf(np.arange(21), p=.2, n=20)),
         alpha=0.75,
        )

# PDF P = .5
plt.scatter(np.arange(21),
            (stats.binom.pmf(np.arange(21), p=.5, n=20)),
            alpha=0.75,
            s=100
       )
plt.plot(np.arange(21),
         (stats.binom.pmf(np.arange(21), p=.5, n=20)),
         alpha=0.75,
        )

# PDF P = .9
plt.scatter(np.arange(21),
            (stats.binom.pmf(np.arange(21), p=.9, n=20)),
            alpha=0.75,
            s=100
       )
plt.plot(np.arange(21),
         (stats.binom.pmf(np.arange(21), p=.9, n=20)),
         alpha=0.75,
        )

# LEGEND
plt.text(x=3.5, y=.075, s="$p = 0.2$", alpha=.75, weight="bold", color="#008fd5")
plt.text(x=9.5, y=.075, s="$p = 0.5$", alpha=.75, weight="bold", color="#fc4f30")
plt.text(x=17.5, y=.075, s="$p = 0.9$", alpha=.75, weight="bold", color="#e5ae38")

# TICKS
plt.xticks(range(21)[::2])
plt.tick_params(axis = 'both', which = 'major', labelsize = 18)
plt.axhline(y = 0, color = 'black', linewidth = 1.3, alpha = .7)

# TITLE, SUBTITLE & FOOTER
plt.text(x = -2.5, y = .37, s = "Binomial Distribution - $p$",
               fontsize = 26, weight = 'bold', alpha = .75)
plt.text(x = -2.5, y = .32,
         s = 'Depicted below are three Binomial distributed random variables with varying $p $. As one can see\nthe parameter $p$ shifts and skews the distribution.',
         fontsize = 19, alpha = .85)
plt.text(x = -2.5,y = -0.065,
         s = '   ©Joshua Görner                                                                                github.com/jgoerner   ',
         fontsize = 14, color = '#f0f0f0', backgroundcolor = 'grey');


"""
Functions that return a NumPy array containing a wave such as
a sine wave or square wave.

Parameters
----------
  frequency: int or float
     wave frequency in Hertz (1/second)
  max_amplitude: float
     The maximum amplitude of the wave
  phase_shift: float
     Initial phase of the wave
  sample_rate: int or float
     Number of samples per second of the waveform
  time_duration: int or float
     The time, in seconds, of the output wave.

Variables
---------
    num_samples is the size of the output array
    time_sequence is an array [0, v, 2*v, .... num_samples*v] where
        (num_samples + 1)*v == time_duration.
"""
import numpy as np
import matplotlib.pyplot as plt

def generate_trignometric_wave(
        func, frequency, max_amplitude,
        phase_shift, sample_rate, time_duration):
    num_samples = int(time_duration * sample_rate)
    time_sequence = np.linspace(0, time_duration, num_samples, endpoint=False)
    return max_amplitude*func(2*np.pi*frequency*time_sequence + phase_shift)

def generate_sine_wave(
        frequency, max_amplitude, phase_shift,
        sample_rate, time_duration):
    return generate_trignometric_wave(
        np.sin, frequency, max_amplitude, phase_shift,
        sample_rate, time_duration)

def generate_cosine_wave(
        frequency, max_amplitude, phase_shift,
        sample_rate, time_duration):
    return generate_trignometric_wave(
        np.cos, frequency, max_amplitude, phase_shift,
        sample_rate, time_duration)

def generate_square_wave(
        frequency, max_amplitude,
        phase_shift, sample_rate, time_duration):
    num_samples = time_duration * sample_rate
    time_sequence = np.linspace(0, time_duration, num_samples, endpoint=False)
    def func(v):
        return - np.sign((v - v.astype(int)) - 0.45)
    return max_amplitude*func(frequency*time_sequence + phase_shift)

def plot_signal(signal, time_duration, sample_rate):
    plt.figure(1)
    plt.clf()
    num_samples = time_duration * sample_rate
    time_sequence = np.linspace(0, time_duration, num_samples, endpoint=False)
    plt.plot(time_sequence, signal)
    plt.show()
    return

# TESTS

def test():
    # Plot
    frequency = 1.0
    max_amplitude = 1.0
    phase_shift = 0.0
    sample_rate = 100
    time_duration = 2

    signal = generate_square_wave(frequency, max_amplitude, phase_shift, sample_rate, time_duration)
    plot_signal(signal, time_duration, sample_rate)

    phase_shift = np.pi/2.0 # 90 degrees
    signal = generate_sine_wave(frequency, max_amplitude, phase_shift, sample_rate, time_duration)
    plot_signal(signal, time_duration, sample_rate)

    signal = generate_cosine_wave(frequency, max_amplitude, phase_shift, sample_rate, time_duration)
    plot_signal(signal, time_duration, sample_rate)

if __name__ == '__main__':
    test()


import pandas as pd
import numpy as np
import scipy.stats as stat
import matplotlib as mat
"""
Problem 1:  Generate a random 20x20 grid of two digit natural numbers. Find the largest product of four
            diagonally adjacent numbers.
"""
a = np.random.randint(10, 99, (20, 20))

def largest_product(a):
    prd = 0
    d = dict()
    l = []
    for row in np.arange(0,a.shape[0]-3):
        for col in np.arange(0,a.shape[0]-3):
            temp_down = a[row, col]*a[row+1, col+1]*a[row+2, col+2]*a[row+3, col+3]
            temp_up = a[row+3, col]*a[row+2, col+1]*a[row+1, col+2]*a[row, col+3]
            if temp_up > temp_down:
                temp = temp_up
                address = f"Up Diag {(row+3, col),(row+2,col+1),(row+1,col+2),(row,col+3)}"
            else:
                temp = temp_down
                address = f"Down Diag {(row, col),(row+1,col+1),(row+2,col+2),(row+3,col+3)}"
            l.append([temp,temp_up,temp_down])
            if temp > prd:
                prd = temp
                d = {"product": prd,
                  "address": address}
    return d

print(f"Maximum Product: {largest_product(a)['product']} \nAddress: {largest_product(a)['address']}")

"""
Probelm 2:  The Social Security administration has this neat data by year of what names are most popular
            by gender for born that year in the USA. The popular name list for the year 1990 is given in the
            website <url>. Write a python program to identify the names that appear in the list of popular
            names of both genders.
"""

data = pd.read_csv("popular_names.csv")

def popular_name(data):

    males = set(data['Male name'])
    females = set(data['Female name'])
    common = males.intersection(females)
    d = dict()
    for name in common:
        combined_rank = int(data['Rank'][data['Male name']== name] ) + int(data['Rank'][data['Female name']== name])
        d.update({combined_rank:name})
        #print(f"Name: {name},\t Rank: {combined_rank}")

    name = d[min(d.keys())]

    return name

print(f"Most Popular name {popular_name(data)}")

"""
Problem 3: Invert matrix using first principle
"""

#b = np.random.randint(10, 99, (3, 3))

def matrix_invert_first_principles(b):

    if b.shape[0] != b.shape[1]:
        return print("Please input a square matrix")

    b = b.astype("float")
    c = b.copy()
    I = np.eye(b.shape[0], b.shape[1])

    for i in np.arange(0, b.shape[0]):
        I[i] = I[i, :] / b[i, i]
        b[i] = b[i, :] / b[i, i]
        for j in np.arange(0, b.shape[0]):
            if j == i:
                pass
            else:
                I[j] = I[j] - b[j, i] * I[i]
                b[j] = b[j] - b[j, i] * b[i]

    def check_result(result, actual):

        res = np.matmul(result, actual).astype("int")
        #print(res)
        if (np.eye(result.shape[0], result.shape[1]) == res).all():
            ok = True
            d_res = None
        else:
            ok = False
            d_res = {"input": actual}

        return ok, d_res

    if not check_result(I, c)[0]:
        return (f"Something not ok. Input {check_result(I, c)[1]['input']}")

    return I

# b = np.array(  [[35, 40, 82],
#                 [68, 86, 29],
#                 [38, 53, 13]])

print(f"Result Problem 3: {matrix_invert_first_principles(b)}")

"""
Problem 5:  Numerical Integration using Monte Carlo simulation.

"""

def integrate_func_monte_carlo(func,a,b,N=1000):
    x = np.random.uniform(a, b, N)
    res = func(x)
    return ((b-a)/N)*sum(res)

def func0(x):
    return x

def func_a(x):
    return np.exp(-x**2)

def func_b(x):
    return 1/(1 + x**2)

def func_c(x):
    return np.sqrt(x**4 + 1)

def confidence_interval(arr, p = 0.95):

    u = arr.mean()
    sd = arr.std()
    z = stat.norm.ppf(1-(1-p)/2)

    ci = z*(sd/np.sqrt(len(arr)))
    d = {"mean": u,
         "interval":ci}
    return d

def compute_value(func,a,b,theo_vaule,N=1000):
    print(N)
    r = []
    for _ in np.arange(0,100):
        r.append(integrate_func_monte_carlo(func, a, b, N))

    result = np.array(r)
    diff_arr = result - theo_vaule
    result_dict = {"result":result}
    result_dict.update(confidence_interval(diff_arr))

    return result_dict

l_N = [10, 50, 100, 500, 1000, 5000, 10000, 50000, 1000000]
#solutions :

solution_a = [compute_value(func_a, 0, 1, 0.5, N) for N in l_N]
solution_b = [compute_value(func_b, 1, 2, 0.5, N) for N in l_N]
solution_c = [compute_value(func_c, 0, 1, 0.5, N) for N in l_N]


import scipy as sp
import scipy.linalg as la

def Problem2(Q,b,x0,tol=1e-10):
    """Use conjugate gradient method to minimize Ax-b"""

    x = x0.copy()
    g = Q.dot(x)-b
    d = -g

    ndim = x.size

    for iter in range(ndim):
        if la.norm(g)<tol:
            break
        a = -1.0*(g.T.dot(d))/(d.T.dot(Q).dot(d))
        x += a*d
        g = Q.dot(x)-b
        beta = (g.T.dot(Q).dot(d))/(d.T.dot(Q).dot(d))
        d = -g+beta*d

    return x

def Problem1(A,b,x0,tol=1e-7,maxiter=5000):
    """Find the minimum of a function"""

    x = x0.copy()
    Q = A.T.dot(A)
    niter = maxiter
    while la.norm(A.dot(x)-b)>tol and maxiter>0:
        grad_f = 2.0*A.T.dot(A.dot(x)-b)
        alpha_f = (grad_f.T.dot(grad_f))/(4.0*grad_f.T.dot(Q.dot(grad_f)))
        x = x + -1.0*alpha_f*grad_f
        niter -= 1

    return x, maxiter-niter

"""
Explanations:
    We found that 4.0*denominator of alpha_f converged faster than just having the denominator.

    I wasn't able to get the eigenvector of A.T.dot(A) to converge in 1 iteration.  The lowest convergence happened in 236 iterations

"""


import numpy as np
a = np.array([1, 2, 3])
print(a)
print(a.shape) # tuple of ndim elements
print(a.dtype) # data types
print(a.ndim) # number of dimensions
print(a.size) # total number of elements
print(a.itemsize) # return size of each elements in bytes

b = a * np.array([2, 0, 2]) # element wise multiplication
c = a + np.array([4, 2, 3]) # element wise summation
print(b)

print(type(b))

# dot product
l1 = np.array([1, 2, 3])
l2 = np.array([4, 5, 6])
dot = np.dot(l1, l2) # dot = l1 @ l2
print(dot)

# boolean indexing
bool_arr = a > 1
print(bool_arr, a[bool_arr]) # a[bool_arr] will have rank 1
b = np.where(a > 1, a, -1)
print(b)

# fancy indexing
a = np.array([10, 19, 30, 41, 50, 61])
idx = [1, 3, 5]
print(a[idx])

even_idx = np.argwhere(a % 2 == 0).flatten()
print(a[even_idx])

a = np.arange(1, 7)
print(a, a.shape)
b = a[:, np.newaxis]
print(b)
b = a[np.newaxis, :]
print(b)

# concatenation
print('Concatenation')
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6]])
c = np.concatenate((a, b))
print(c)
c = np.concatenate((a, b), axis = 0)
print(c)
c = np.concatenate((a, b), axis = None)
print(c)

# hstack, vstack to concatenate horizontaly or verticaly.

# generate arrays
a = np.zeros((2, 3)) # deefault data types float64
a = np.ones((2, 3))
a = np.eye(3)
a = np.full((2, 3), 5.0)
print(a)
a = np.linspace(0, 10, 5)
print(a)
a = np.random.random((3, 2)) # unifor distribution
print(a)
a = np.random.randn(2, 3) # gaussian or normal distribution, mean = 0, var = 1

a = np.random.randint(2, 10, size = (2, 2))
a = np.random.choice([-2, -1, 5], size = 5)
print(a)

# linalg
a = np.array([[1, 2], [3, 4]])
eigenvalues, eigenvectors = np.linalg.eig(a)
print(eigenvalues)
print(eigenvectors) #column vector
# e_vec * e_val = A * e_vec
b = eigenvectors[:, 0] * eigenvalues[0]
c = a @ eigenvectors[:, 0]
print(b == c)
print(np.allclose(b, c))


# Program the function distance(p1, p2) which returns the distance between the points p1 and p2 in
# n-dimensional space. p1 and p2 will be given as arrays.
#
# Your program should work for all lengths of arrays, and should return -1 if the arrays aren't of
# the same length or if both arrays are empty sets.
#
# If you don't know how to measure the distance between two points,
# go here: http://mathworld.wolfram.com/Distance.html
import numpy as np

def distance(p1, p2):
    p1 = np.asarray(p1)
    p2 = np.asarray(p2)

    # compare if both arrays are of same shape, and neither one of them of size zero
    if p1.shape == p2.shape and (np.size(p1) != 0 or np.size(p2) != 0):
        return np.linalg.norm(p1 - p2)
    else:
        return -1

print(distance([2, 2], [1, 1]))
print(distance([1], [1, 1, 1, 1, 1, 1, 1, 1, 1]))
# test.describe("Normal cases")
# test.assertEqual(distance([2,2],[1,1]), 2**0.5)
# test.assertEqual(distance([4],[1]), 3)
# test.assertEqual(distance([1,1,1],[0,0,0]), 3**0.5)
# test.assertEqual(distance([2,1,3,1],[2,0,2,-1]), 6**0.5)
# test.assertEqual(distance([3,2,3],[0,1,1]), 14**0.5)

# test.describe("Bad input/edge cases")
# test.assertEqual(distance([],[]), -1)
# test.assertEqual(distance([1],[1,1,1,1,1,1,1,1,1]), -1)