'''
Exp 11: Program to demonstrate use of NumPy: Array objects.

Theory:
NumPy is a Python library used for working with arrays.
It also has functions for working in domain of linear algebra, fourier transform, and matrices.
NumPy was created in 2005 by Travis Oliphant. It is an open source project and you can use it freely.
NumPy stands for Numerical Python.

In Python we have lists that serve the purpose of arrays, but they are slow to process.
NumPy aims to provide an array object that is up to 50x faster than traditional Python lists.
The array object in NumPy is called ndarray, it provides a lot of supporting functions that make working with ndarray very easy.
0-D Arrays
0-D arrays, or Scalars, are the elements in an array. Each value in an array is a 0-D array.
1-D Arrays
An array that has 0-D arrays as its elements is called uni-dimensional or 1-D array.
These are the most common and basic arrays.
2-D Arrays
An array that has 1-D arrays as its elements is called a 2-D array.
These are often used to represent matrix or 2nd order tensors.

arange() is an inbuilt numpy function that returns an ndarray object containing evenly spaced values within a defined interval.
linspace() function returns number spaces evenly w.r.t interval.
Similar to numpy.arange() function but instead of step it uses sample number.
logspace() function returns number spaces evenly w.r.t interval on a log scale.
zeros() function returns a new array of given shape and type, with zeros.
ones() function returns a new array of given shape and type, with ones.
empty() is used to create an array without initializing the entries of given shape and type.
identity() Return a identity matrix i.e. a square matrix with ones on the main diagonal.

Slicing:
Slicing in python means taking elements from one given index to another given index.
We pass slice instead of index like this: [start:end].
We can also define the step, like this: [start:end:step].
If we don't pass start its considered 0
If we don't pass end its considered length of array in that dimension
If we don't pass step its considered 1

@author: KHAN FARHAN NADEEM (19CO27)
'''

import numpy as np
#############################
#    creating numpy arrays  #
#############################
#########################################
# create 1D arrays using array function #
#########################################
arr1d = np.array([1,2])

#######################################################
# create 2D arrays using array function of type float #
#######################################################
arr2d = np.array([[1,2,3],[4,5,6],[7,8,9]],dtype=float)

######################################
# create array using arange function #
######################################
arr_ar = np.arange(1,100,9)

########################################
# create array using linspace function #
########################################
arr_lp = np.linspace(10,11,10)

########################################
# create array using logspace function #
########################################
arr_logp = np.logspace(10,15,10)

########################################
# create array using various functions #
########################################
arr_zeros = np.zeros((2,3))
arr_ones = np.ones((3,2))
arr_empty = np.empty((2,4))
arr_identity = np.identity(3)

##############################
## accessing array elements ##
##############################
print('\n arr1d =',arr1d)
print('\n arr2d =')

for row in arr2d:
    print(row)

print('\narr_ar =',arr_ar)
print('\narr_lp =',arr_lp,' having size of',arr_lp.size)
print('\narr_logp =',arr_logp,' having size of',arr_logp.size)
print('\narr_zeros =\n',arr_zeros)
print('\narr_ones =\n',arr_ones)
print('\narr_empty =\n',arr_empty)
print('\narr_identity =\n',arr_identity)

###################################
###  performing array operations ##
###################################
print('\nPerforming Array Operations:')
print('Addition of 2D arrays\n',arr2d + arr_identity)
print('Substraction of 2D arrays\n',arr2d - arr_identity)
print('Multiplication of 2D arrays\n',arr2d * arr_identity)
print('Matrix Multiplication of 2D arrays\n',arr2d @ arr_identity)
print('Transpose of 2D arrays\n',arr2d.transpose())

###############################
# performing slice operations #
###############################
print('Elements of Second Row and First 2 Columns',arr2d[1,:2])
print('Elements of Last Row and Second Column onwards',arr2d[-1,1:])
print(arr2d[arr1d])

################################
# performing reshape on arrays #
################################

##################################
# reshaping 1D array to 2D array #
##################################
newarr = arr1d.reshape(1,2)
print('reshaped array : ',newarr)

'''
Conclusion:
NumPy is a fundamental package for scientific computation & mathematical operations in python.
NumPy is way more powerful than Lists.
'''


import numpy as np

def softmax(X):
    e = np.exp(X - np.amax(X, axis=1, keepdims=True))
    return e/np.sum(e, axis=1, keepdims=True)

def categorical_cross_entropy(y_pred, y_true, eps=1e-15):
    # Assumes one-hot encoding.
    y_pred = np.clip(y_pred, eps, 1 - eps)
    # XXX: do we need to normalize?
    y_pred /= y_pred.sum(axis=1, keepdims=True)
    loss = -np.sum(y_true * np.log(y_pred), axis=1)
    return loss

def one_hot_encode(labels, n_classes, out=None):
    out_shape = (labels.size, n_classes)
    if labels.dtype != np.dtype(int):
        raise ValueError('labels.dtype must be int')
    if out is None:
        out = np.empty(out_shape)
    else:
        if out.shape != out_shape:
            raise ValueError('shape mismatch')
    out.fill(0)
    if labels.size == 1:
        out[0, labels] = 1
    else:
        for c in range(n_classes):
            out[labels == c, c] = 1
    return out

def one_hot_decode(one_hot, out=None):
    out_shape = (one_hot.shape[0],)
    if out is None:
        out = np.empty(out_shape, dtype=np.dtype(int))
    else:
        if out.dtype != np.dtype(int):
            raise ValueError('out.dtype must be int')
        if out.shape != out_shape:
            raise ValueError('shape mismatch')
    result = np.argmax(one_hot, axis=1)
    np.copyto(out, result)
    return out


import numpy as np

def euclidean_distance(u, v):
    """
    Args:
        u (array-like): Vector
        v (array-like): Vector

    Returns:
        The euclidean distance between vectors u and v.

    """
    return np.linalg.norm(np.array(u) - np.array(v))

def manhattan_distance(u, v):
    """
    Args:
        u (array-like): Vector
        v (array-like): Vector

    Returns:
        The manhattan distance between vectors u and v.

    """
    return np.abs(np.array(u) - np.array(v)).sum()

def hamming_distance(u, v):
    """
    Args:
        u (array-like): Vector
        v (array-like): Vector

    Returns:
        The hamming distance between vectors u and v.

    """
    return np.count_nonzero(np.array(u) != np.array(v))


import numpy as np

def build_stretch_matrix(x, y):
    stretch_transform = np.array(
        [[x, 0, 0],
         [0, y, 0],
         [0, 0, 1]],
        dtype=np.float32)
    return stretch_transform

def build_shear_matrix(x, y):
    tan_x = np.tan(x)
    tan_y = np.tan(y)
    shear_transform = np.array(
        [[1,     tan_x, 0],
         [tan_y, 1,     0],
         [0,     0,     1]],
        dtype=np.float32)
    return shear_transform

def build_rotate_matrix(t):
    cos_t = np.cos(t)  # t must be in radians
    sin_t = np.sin(t)
    rotate_transform = np.array(
        [[cos_t,  sin_t, 0],
         [-sin_t, cos_t, 0],
         [0,          0, 1]],
        dtype=np.float32)
    return rotate_transform

def build_flip_matrix(h, v):
    # reflect about x and y
    if h and v:
        flip_transform = np.array(
            [[-1,  0, 0],
             [ 0, -1, 0],
             [ 0,  0, 1]], dtype=np.float32)
    # reflect about only x
    elif h and not v:
        flip_transform = np.array(
            [[ 1,  0, 0],
             [ 0, -1, 0],
             [ 0,  0, 1]], dtype=np.float32)
    # reflect about only y
    elif not h and v:
        flip_transform = np.array(
            [[-1,  0, 0],
             [ 0,  1, 0],
             [ 0,  0, 1]], dtype=np.float32)
    # do not reflect
    else:
        flip_transform = np.eye(3, dtype=np.float32)

    return flip_transform

def build_translate_matrix(x, y):
    translate_transform = np.array(
        [[1, 0, x],
         [0, 1, y],
         [0, 0, 1]], dtype=np.float32)
    return translate_transform

def build_transformation_matrix(
        imsize, theta=None, offset=None, flip=None, shear=None, stretch=None):

    # use the identity matrix as default
    if theta is None:
        theta = 0.
    if offset is None:
        offset = (0., 0.)
    if flip is None:
        flip = (False, False)
    if shear is None:
        shear = (0., 0.)
    if stretch is None:
        stretch = (1., 1.)

    cx, cy = np.array(imsize) / 2 - 0.5
    center_matrix = build_translate_matrix(cx, cy)
    stretch_matrix = build_stretch_matrix(*stretch)
    shear_matrix = build_shear_matrix(*shear)
    rotate_matrix = build_rotate_matrix(theta)
    flip_matrix = build_flip_matrix(*flip)
    uncenter_matrix = build_translate_matrix(-cx, -cy)
    translate_matrix = build_translate_matrix(*offset)

    transform_matrix = center_matrix.dot(
        stretch_matrix).dot(
        shear_matrix).dot(
        rotate_matrix).dot(
        flip_matrix).dot(
        uncenter_matrix).dot(
        translate_matrix
    )

    return transform_matrix


"""
Intensive transformation is an basic gray level transformation
Formula:

    X = ( 255 / (high - low) ) * ( X - low)

    X: The intensive in each pixel
    255: The max level intensive of image
    high: The highest gray level of image
    low: The lowest gray level of image

"""

import cv2
import numpy as np
from __utils__.general import show_image
from histogram.plot_histogram import use_calc_hist_in_cv2_function

def contrast_schetching(image):
    # Get the high and low level intensive
    high = image.max()
    low = image.min()

    # Copy gray image
    image_gray = np.copy(image)

    # Contrast stretching
    for pixel in np.nditer(image_gray, op_flags=['readwrite']):
        pixel[...] = np.round(np.abs((255 / float(high - low)) * (pixel - low)))
    return image_gray

if __name__ == '__main__':
    img = cv2.imread('../../asserts/images/wiki.jpg')
    img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
    res = contrast_schetching(img)
    show_image(np.hstack((img, res)))


'''
K Nearest Neighbor implementations for the points
    data = [[(1,6), 7], [(2,4), 8], [(3,7), 16], [(6,8),44], [(7,1),50], [(8,4),68]]
Seeking the point
    q = (4,2)
Using two different distance formulas: Manhathan and euclidean_distance
If the the subsequent elements to k have the same distance as k, those are
also included.

'''

import math
import numpy as np

def manh_dist(a, b):
    return abs(a[0]-b[0]) + abs(a[1]-b[1])

def euclidean_dist(a,b):
    return math.sqrt(math.pow(a[0]-b[0], 2)+ math.pow(a[1]-b[1], 2))

def KNN(k, data, q, dist):
    r = sorted([(dist(a[0],q), index) for index, a in enumerate(data)])
    avg = [data[b][1] for a, b in r[:k]]

    # Check if the distance value is the same for the elements after k
    # if so add them to the list to get the averages.
    max_k_value = r[k-1][0]
    start_index = k
    while True:
        if r[start_index][0] == max_k_value:
            avg.append(data[r[start_index][1]][1])
            start_index += 1
        else: break
    average = np.mean(avg)
    print 'Average for {} Neighbors, using {} as distance is: {}'.format(
        k, dist.__name__, average
    )

if __name__ == '__main__':
    data = [[(1,6), 7], [(2,4), 8], [(3,7), 16], [(6,8),44], [(7,1),50], [(8,4),68]]
    q = (4,2)
    KNN(1, data, q, manh_dist)
    KNN(3, data, q, manh_dist)
    KNN(1, data, q, euclidean_dist)
    KNN(3, data, q, euclidean_dist)


"""
Demo of Runge Phenomenon

Author: HearyShen
Date: 2020.10.28
"""
import matplotlib.pyplot as plt
import numpy as np

LEFT_BOUND = -1
RIGHT_BOUND = 1.01

PLT_ROWS = 2
PLT_COLS = 3

def runge_function(x):
    y = 1 / (1 + 25 * x**2)     # Runge Function
    return y

def runge_polyfit(degree):
    # original runge function
    x_orig = np.arange(LEFT_BOUND, RIGHT_BOUND, 0.01)
    y_orig = runge_function(x_orig)

    # sample points
    interval = 2 / degree
    x_sample = np.arange(LEFT_BOUND, RIGHT_BOUND, interval)
    y_sample = runge_function(x_sample)

    plt.title(f"{degree}-degree polyfit")

    # plot original runge function
    plt.plot(x_orig, y_orig, label="runge")

    # plot runge sample points
    plt.plot(x_sample, y_sample, "rx")

    coef = np.polyfit(x_sample, y_sample, degree)
    # y_fit = sum([coef[i] * (x**(degree-i)) for i in range(degree+1)])
    y_fit = np.polyval(coef, x_orig)
    plt.plot(x_orig, y_fit, label=f"polyfit-{degree}")

    plt.legend()
    # plt.show()

def runge_phenomenon(degrees=[1, 5, 9, 13, 15, 17]):
    plt.figure(figsize=(16, 9))
    for i in range(min(PLT_ROWS*PLT_COLS, len(degrees))):
        # plot each polynomial function of each degree
        plt.subplot(PLT_ROWS, PLT_COLS, i+1)

        # plot runge function
        runge_polyfit(degrees[i])

    plt.show()

if __name__ == "__main__":
    runge_phenomenon()


from __future__ import division
import numpy as np

def recursive_trapezoid(n,R,f,a,b):
    '''
    Computes the integral evaluation using the recursive trapezoid rule.
    The function actually evaluates R(n,m) but since m is 0 for us to
    prefer trapezoid rule, we don't accept that as an input.
    It accepts the function 'f' we are evaluating.
    returns R(n,0) added to R.

    The base case of this recursive this function is R(0,0) which
    is assumed to have already been inserted into the dictionary R
    that's why it is not added here.
    '''
    retVal = -1
    if (n,0) in R.keys():
        retVal = R[(n,0)]
    elif n == 0:
        retVal = 0.5 * (b-a) * (f(a) + f(b))
    else:
        h = (b-a) * (0.5 ** n)
        retVal = 0.5 * recursive_trapezoid(n-1, R, f, a, b) + h * sum([ f(a+(2*k-1)*h) for k in xrange(1,(2**(n-1))+1)])
    return retVal

def get_R(n, m, R, f, a, b):
    '''
    This function computes and returns the value of R with R(n,m) added to it.
    It expects the old R as an input and also the function we are evaluating
    '''
    retVal = -1
    if (n,m) in R.keys():
        retVal = R[(n,m)]
    elif m == 0:
        # call recursive trapezoidal function
        retVal = recursive_trapezoid(n,R,f,a,b)
    else:
        retVal = get_R(n,m-1,R,f,a,b) + (1.0/((4**m)-1)) * (get_R(n,m-1,R,f,a,b) - get_R(n-1,m-1,R,f,a,b))
    return retVal

def integrate(f,a,b,nrows):
    # The R "matrix" which would store the values computed from trapezoidal and
    # Romberg's algorithm
    # This is implemented as a dictionary which stores a tuple (n,m) as the key
    # and the corresponding R(n,m) evaluation as the value against the (n,m) key
    R = {}
    rows = nrows
    for i in range(rows):
        for j in range(i+1):
            R[(i,j)] = get_R(i,j,R,f,a,b)
    return R[i,j], R

def printR(R, n):
    for i in range(rows):
        for j in range(i+1):
            print("{0:.10f}".format(R[(i,j)])),
        print
    print

if __name__ == '__main__':
    rows = 9
    #---------function
    print("Analytical Integral is ln(3) = "),
    Actual = 1.0986122886681098
    print(Actual)
    f = lambda x: 1 / (1+x)
    a = 0
    b = 2
    integral, R = integrate(f,a,b,rows)
    print "Numerical Integral is :",integral
    print("Error is {}".format(abs(Actual-integral)))
    printR(R,rows)
    #---------function 2
    print("Analytical Integral is e^1 - 1 = "),
    Actual = 1.718281828459045
    print(Actual)
    f = lambda x: np.e ** x
    a = 0
    b = 1
    integral, R = integrate(f,a,b,rows)
    print "Numerical Integral is", integral
    print("Error is {}".format(abs(Actual-integral)))
    printR(R,rows)
    #---------function 3
    print("Analytical Integral is 2/3 = "),
    Actual = 0.6666666666666666
    print(Actual)
    f = lambda x: np.sqrt(x)
    a = 0
    b = 1
    integral, R = integrate(f,a,b,rows)
    print "Numerical Integral is", integral
    print("Error is {}".format(abs(Actual-integral)))
    printR(R,rows)


import numpy as np

def split(x, h):
    """ Split the interval

    Args:
        x: list/ndarray, the interval to be splited
        h: double, the step length of the split

    Returns:
        t: ndarray/list, the split of the interval t
    """
    # the endpoints of the interval
    a, b = x
    n = int((b - a)/h)
    t = np.linspace(a, b, n+1)

    return t

def improved_Euler(x, func, ini):
    """ Solve IVP by improved Euler method

    Args:
        x: list/ndarray, the splitiation of the interval
        func: the function f(x, y) which satisfies y'(x) = f(x, y)
        ini: the initial value for IVP

    Returns:
        y: ndarray, the list of values of y at every t
    """
    n = len(x)
    h = x[1] - x[0]

    # begin iteration
    y = np.zeros(n)
    y[0] = ini
    for i in range(1, n):
        y0 = y[i-1] + h*func(x[i-1], y[i-1])
        y[i] = y[i-1] + h*(func(x[i-1], y[i-1]) + func(x[i], y0))/2

    return y

def f1(x, y):
    return y**2

def f2(x, y):
    return x/y

def g(x, y):
    return np.sin(x)/x

if __name__ == '__main__':
    h = 0.1
    # the solution for ordinary differential equation 1
    t1 = [0, 0.4]
    x1 = split(t1, h)
    y1 = improved_Euler(x1, f1, 1)
    y1_true = 1/(1 - x1)
    print("The numerical solution, analytic solution and the error of question 1 are as follows:")
    for item in zip(x1, y1, y1_true):
        print(f"{item[0]:.1f}\t{item[1]:.6f}\t{item[2]:.6f}\t{item[2] - item[1]:.6f}")
    # the solution for ordinary differential equation 2
    t2 = [2.0, 2.6]
    x2 = split(t2, h)
    y2 = improved_Euler(x2, f2, 1)
    y2_true = np.sqrt(x2**2 - 3)
    print("The numerical solution, analytic solution and the error of question 2 are as follows:")
    for item in zip(x2, y2, y2_true):
        print(f"{item[0]:.1f}\t{item[1]:.6f}\t{item[2]:.6f}\t{item[2] - item[1]:.6f}")

    # thinking question
    # calculate the integral \int_0^1 \frac{\sin x}{x} dx
    x3 = split([1e-32, 1], 0.01)
    y3 = improved_Euler(x3, g, 0)
    print(f"The value of Si(1) is {y3[-1]:.5f}.")


import numpy as np

def FaultFreq(n, fr,d,D,phi):
    """
    :param n:   number of rolling elements
    :param fr:  shaft speed
    :param d:   ball diameter
    :param D:   pitch diameter
    :param phi: the angle of load from radial plane
    :return:    four fault frequency
    """
    BPFO = n*fr/2*(1 - d/D*np.cos(phi))
    BPFI = n*fr/2*(1 + d/D*np.cos(phi))
    FTF = fr/2*(1 - d/D*np.cos(phi))
    BSF = D*fr/(d)*(1 - (d/D*np.cos(phi))**2)
    print("BPFO: %.4ffr \nBPFI: %.4ffr \nFTF: %.4ffr \nBSF: %.4ffr"%(BPFI, BPFO, FTF, BSF))
    return BPFO, BPFI, FTF, BSF

if __name__ == '__main__':
    # SKF 6205-2RS JEM: drive end parameters
    n = 9
    fr = 1
    d = 0.3126 * 25.4
    D = 1.537 * 25.4
    phi = 0

    print('Drive end fault frequency:')
    bpfo, bpfi, ftf, bsf = FaultFreq(n, fr, d, D, phi)

    # SKF 6203-2RS JEM: fan end parameters
    n = 8
    fr = 1
    d = 0.2656 * 25.4
    D = 1.122 * 25.4
    phi = 0
    print('\nFan end fault frequency:')
    bpfo, bpfi, ftf, bsf = FaultFreq(n, fr, d, D, phi)


'''
@ author: Tinotenda Mhlanga
@ program: Trapezoidal Rule

'''

from math import sin, pi, exp, factorial
import scipy.integrate

f = lambda x: exp(-x)

mac_series = 0
x1 = 1
a = 0
b = 1
n = 100 # number of divisions
h = (b - a) / n # step size
S = 0.5 * (f(a) + f(b))

# Trapezoidal Rule
for i in range(1, n):
	# Summation
	S += f(a + i*h)

I = h * S

# Maclaurin Expansion
for i in range(1, 8):
	if i % 2 == 0:
		mac_series -= x1**i/(i * factorial(i - 1))
	else:
		mac_series += x1**i/(i * (factorial(i - 1)))

# Exact Value
exact_value = scipy.integrate.quad(f, 0, 1)

print(f'\nExact Value: {round(exact_value[0], 12)}')
print(f'\nResult Using Trapezoidal Rule: {round(I, 12)}')
print(f'\nResult Using Maclaurins Expansion: {round(mac_series, 12)}')


import numpy as np
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from itertools import product, combinations, combinations_with_replacement
import math
import re
import vtk_visualizer as vv
import numpy as np
import sys
from PyQt5.QtWidgets import *

##http://mathworld.wolfram.com/topics/Surfaces.html

def showFigure(filename):
    fig = plt.figure()
    ax = fig.gca(projection='3d')
    ax.set_aspect("equal")
    file_object = None
    try:
      file_object = open(filename, "r")
    except IOError:
        print("     ... file " + filename+" not exits!\n")
        return False
    vtkControl = vv.VTKVisualizerControl()
    a = []

    for line in file_object:
        numbers = (re.findall(r"[-+]?\d*\.\d+|\d+", line))
        x = float(numbers[0])
        y = float(numbers[1])
        z = float(numbers[2])
        a.append([x,y,z])

    b = np.array(a)
    vtkControl.AddPointCloudActor(b)
    app = QApplication.instance()
    if app is None:
        app = QApplication(sys.argv)
    app.exec_()
    file_object.close()
#end def

def calculateTorus(R,r):
    file_object  = open("torus.txt", "w")
    for theta in np.arange(0,2*np.pi,0.05):
        for phi in np.arange(0,2*np.pi,0.05):
                x = (R + r * np.cos(phi)) * np.cos(theta)
                y = (R + r * np.cos(phi)) * np.sin(theta)
                z = r * np.sin(phi)
                file_object.write(str(x)+" "+str(y)+" "+str(z)+"\n")
    file_object.close()
#end def

def calculateDrawTorus(R,r):
    fig = plt.figure()
    ax = fig.gca(projection='3d')
    ax.set_aspect("equal")
    for theta in np.arange(0,2*np.pi,0.4):
        for phi in np.arange(0,2*np.pi,0.4):
            ax.scatter((R + r * np.cos(phi)) * np.cos(theta),
                    (R + r * np.cos(phi)) * np.sin(theta),
                    r * np.sin(phi),
                    color="r", marker='o')
    plt.show()
#end def

def calculateTetrahedron():
    file_object  = open("tetrahedron.txt", "w")
    numbers = []
    for i in np.arange(0,1.1,0.05):
        numbers.append(i)
    for x,y,z in combinations(numbers, 3):
    	file_object.write(str(x)+" "+str(y)+" "+str(z)+"\n")
    file_object.close()
#end def

def calculateDrawTetrahedron():
    fig = plt.figure()
    ax = fig.gca(projection='3d')
    ax.set_aspect("equal")
    numbers = []
    for i in np.arange(0,1.1,0.1):
        numbers.append(i)
    for x,y,z in combinations(numbers, 3):
    	ax.scatter(x, y, z, color="b", marker='o')
    plt.show()
#end def

def calculateTriangle():
    file_object  = open("triangle.txt", "w")
    numbers = []
    for u in np.arange(0,4,0.05):
        for v in np.arange(0,((4-u)/2),0.05):
            x = u
            y = v
            z = 4-u-2*v
            file_object.write(str(x)+" "+str(y)+" "+str(z)+"\n")
    file_object.close()
#end def

def calculateDrawTriangle():
    fig = plt.figure()
    ax = fig.gca(projection='3d')
    ax.set_aspect("equal")
    for u in np.arange(0,4,0.2):
        for v in np.arange(0,((4-u)/2),0.2):
            ax.scatter(u, v, 4-u-2*v,
                color="r", marker='o')
    plt.show()
#end def

def calculateCube():
    file_object  = open("cube.txt", "w")
    numbers = []
    for i in np.arange(0,1.05,0.05):
        numbers.append(i)
    for x,y,z in product(numbers, repeat=3):
        if x == 0 or x == 1 or y == 0 or y == 1 or z == 0 or z == 1:
            file_object.write(str(x)+" "+str(y)+" "+str(z)+"\n")
    file_object.close()
#end def

def calculateDrawCube():
    fig = plt.figure()
    ax = fig.gca(projection='3d')
    ax.set_aspect("equal")
    numbers = []
    for i in np.arange(0,1.1,0.2):
        numbers.append(i)
    for x,y,z in product(numbers, repeat=3):
        if x == 0 or x == 1 or y == 0 or y == 1 or z == 0 or z == 1:
            ax.scatter(x, y, z, color="b", marker='o')
    plt.show()
#end def

def calculateSphere():
    file_object  = open("sphere.txt", "w")
    for u in np.arange(0, 2*np.pi, 0.1):
        for v in np.arange(0, 2*np.pi, 0.1):
            x = np.cos(u)*np.sin(v)
            y = np.sin(u)*np.sin(v)
            z = np.cos(v)
            file_object.write(str(x)+" "+str(y)+" "+str(z)+"\n")
    file_object.close()
#end def

def calculateDrawSphere():
    fig = plt.figure()
    ax = fig.gca(projection='3d')
    ax.set_aspect("equal")
    u, v = np.mgrid[0:2*np.pi:50j, 0:np.pi:25j]
    x = np.cos(u)*np.sin(v)
    y = np.sin(u)*np.sin(v)
    z = np.cos(v)
    ax.scatter(x, y, z, color="r", marker='o')
    plt.show()
#end def

def calculateCylinder():
    file_object  = open("cylinder.txt", "w")

    for u in np.arange(0, 2*np.pi, 0.1):
        for z in np.arange (0,10,0.1):##danger
            x = np.cos(u)
            y = np.sin(u)
            file_object.write(str(x)+" "+str(y)+" "+str(z)+"\n")

    for i in np.arange(1,0,-0.1):
        for u in np.arange(0, 2*np.pi, 0.1):
            x = i*np.cos(u)
            y = i*np.sin(u)
            file_object.write(str(x)+" "+str(y)+" "+str(0)+"\n")
            file_object.write(str(x)+" "+str(y)+" "+str(z)+"\n")

    file_object.close()
#end def

def calculateDrawCylinder():
    fig = plt.figure()
    ax = fig.gca(projection='3d')
    ax.set_aspect("equal")
    u = np.mgrid[0:2*np.pi:40j]
    for z in range (0,40):
        x = np.cos(u)
        y = np.sin(u)
        ax.scatter(x, y, z, color="r", marker='o')
    for i in np.arange(1,-0.1,-0.1):
        x = i*np.cos(u)
        y = i*np.sin(u)
        ax.scatter(x, y, z, color="r", marker='o')
        ax.scatter(x, y, 0, color="r", marker='o')
    plt.show()
#end def

def calculateParaboloid():
    file_object  = open("paraboloid.txt", "w")
    for u in np.arange(0, 2*np.pi, 0.05):
        for v in np.arange(0, np.pi, 0.05):
            x = 1*np.cos(v)*np.cos(u)
            y = 1*np.cos(v)*np.sin(u)
            z = np.cos(v)*np.cos(v)
            file_object.write(str(x)+" "+str(y)+" "+str(z)+"\n")
    file_object.close()
#end def

def calculateDrawParaboloid():
    fig = plt.figure()
    ax = fig.gca(projection='3d')
    ax.set_aspect("equal")
    u, v = np.mgrid[0:2*np.pi:20j, 0:np.pi:20j]
    x = 1*np.cos(v)*np.cos(u)
    y = 1*np.cos(v)*np.sin(u)
    z = np.cos(v)*np.cos(v)
    ax.scatter(x, y, z, color="r", marker='o')
    plt.show()
#end def

opc = -1
while opc != 0:
    print(" Menu \n     0. Exit."
        + "\n   1. Calculated solids."
        + "\n   2. Draw torus."
        + "\n   3. Draw tetrahedron."
        + "\n   4. Draw triangle."
        + "\n   5. Draw cube."
        + "\n   6. Draw sphere."
        + "\n   7. Draw cylinder."
        + "\n   8. Draw paraboloid.")

    opc = int(input("Enter option: "))
    if opc == 0:
        print("     ... Bye!\n")
    elif opc == 1:
        calculateTorus(4,0.5)
        calculateTetrahedron()
        calculateTriangle()
        calculateCube()
        calculateSphere()
        calculateCylinder()
        calculateParaboloid()
    elif opc == 2:
        showFigure("torus.txt")
    elif opc == 3:
        showFigure("tetrahedron.txt")
    elif opc == 4:
        showFigure("triangle.txt")
    elif opc == 5:
        showFigure("cube.txt")
    elif opc == 6:
        showFigure("sphere.txt")
    elif opc == 7:
        showFigure("cylinder.txt")
    elif opc == 8:
        showFigure("paraboloid.txt")
    elif opc == 9:
        calculateDrawTorus(4,0.5)
    elif opc == 10:
        calculateDrawTetrahedron()
    elif opc == 11:
        calculateDrawTriangle()
    elif opc == 12:
        calculateDrawCube()
    elif opc == 13:
        calculateDrawSphere()
    elif opc == 14:
        calculateDrawCylinder()
    elif opc == 15:
        calculateDrawParaboloid()
    else:
        print("     ... please enter a valid option!\n")
    # end switch case
#end while


import numpy as np

def qing_function(point, d):
    """
    Qing function.

    Parameters
    ----------
    point : 1-D array with shape (d, )
            A point used to evaluate the function.
    d : integer
        Dimension.

    Returns
    -------
    function value : float

    """
    return np.sum((point ** 2 - np.arange(1, d + 1)) ** 2)

def qing_gradient(point, d):
    """
    Qing gradient.

    Parameters
    ----------
    point : 1-D array with shape (d, )
            A point used to evaluate the function.
    d : integer
        Dimension.

    Returns
    -------
    gradient : 1-D array with shape (d, )

    """
    return 4 * point * (point ** 2 - np.arange(1, d + 1))


import paddle
import numpy as np
from typing import Tuple, Optional, Union

# https://github.com/kaldi-asr/kaldi/blob/cbed4ff688/src/feat/feature-window.cc#L109
def povey_window(frame_len:int) -> np.ndarray:
    win = np.empty(frame_len)
    a = 2 * np.pi / (frame_len -1)
    for i in range(frame_len):
        win[i] = (0.5 - 0.5 * np.cos(a * i) )**0.85
    return win

def hann_window(frame_len:int) -> np.ndarray:
    win = np.empty(frame_len)
    a = 2 * np.pi / (frame_len -1)
    for i in range(frame_len):
        win[i] = 0.5 - 0.5 * np.cos(a * i)
    return win

def sine_window(frame_len:int) -> np.ndarray:
    win = np.empty(frame_len)
    a = 2 * np.pi / (frame_len -1)
    for i in range(frame_len):
        win[i] = np.sin(0.5 * a * i)
    return win

def hamm_window(frame_len:int) -> np.ndarray:
    win = np.empty(frame_len)
    a = 2 * np.pi / (frame_len -1)
    for i in range(frame_len):
        win[i] = 0.54 - 0.46 * np.cos(a * i)
    return win

def get_window(wintype:Optional[str], winlen:int) -> np.ndarray:
    """get window function

    Args:
        wintype (Optional[str]): window type.
        winlen (int): window length in samples.

    Raises:
        ValueError: not support window.

    Returns:
        np.ndarray: window coeffs.
    """
    # calculate window
    if not wintype or wintype == 'rectangular':
        window = np.ones(winlen)
    elif wintype == "hann":
        window = hann_window(winlen)
    elif wintype == "hamm":
        window = hamm_window(winlen)
    elif wintype == "povey":
        window = povey_window(winlen)
    else:
        msg = f"{wintype} Not supported yet!"
        raise ValueError(msg)
    return window

def dft_matrix(n_fft:int, winlen:int=None, n_bin:int=None) -> Tuple[np.ndarray, np.ndarray, int]:
    # https://en.wikipedia.org/wiki/Discrete_Fourier_transform
    # (n_bins, n_fft) complex
    if n_bin is None:
        n_bin = 1 + n_fft // 2
    if winlen is None:
        winlen = n_bin
    # https://github.com/numpy/numpy/blob/v1.20.0/numpy/fft/_pocketfft.py#L49
    kernel_size = min(n_fft, winlen)

    n = np.arange(0, n_fft, 1.)
    wsin = np.empty((n_bin, kernel_size)) #[Cout, kernel_size]
    wcos = np.empty((n_bin, kernel_size)) #[Cout, kernel_size]
    for k in range(n_bin): # Only half of the bins contain useful info
        wsin[k,:] = -np.sin(2*np.pi*k*n/n_fft)[:kernel_size]
        wcos[k,:] = np.cos(2*np.pi*k*n/n_fft)[:kernel_size]
    w_real = wcos
    w_imag = wsin
    return w_real, w_imag, kernel_size

def dft_matrix_fast(n_fft:int, winlen:int=None, n_bin:int=None) -> Tuple[np.ndarray, np.ndarray, int]:
    # (n_bins, n_fft) complex
    if n_bin is None:
        n_bin = 1 + n_fft // 2
    if winlen is None:
        winlen = n_bin
    # https://github.com/numpy/numpy/blob/v1.20.0/numpy/fft/_pocketfft.py#L49
    kernel_size = min(n_fft, winlen)

    # https://en.wikipedia.org/wiki/DFT_matrix
    # https://ccrma.stanford.edu/~jos/st/Matrix_Formulation_DFT.html
    weight = np.fft.fft(np.eye(n_fft))[:self.n_bin, :kernel_size]
    w_real = weight.real
    w_imag = weight.imag
    return w_real, w_imag, kernel_size

def bin2hz(bin:Union[List[int], np.ndarray], N:int, sr:int)->List[float]:
    """FFT bins to Hz.

    http://practicalcryptography.com/miscellaneous/machine-learning/intuitive-guide-discrete-fourier-transform/

    Args:
        bins (List[int] or np.ndarray): bin index.
        N (int): the number of samples, or FFT points.
        sr (int): sampling rate.

    Returns:
        List[float]: Hz's.
    """
    hz = bin * float(sr) / N

def hz2mel(hz):
    """Convert a value in Hertz to Mels

    :param hz: a value in Hz. This can also be a numpy array, conversion proceeds element-wise.
    :returns: a value in Mels. If an array was passed in, an identical sized array is returned.
    """
    return 1127 * np.log(1+hz/700.0)

def mel2hz(mel):
    """Convert a value in Mels to Hertz

    :param mel: a value in Mels. This can also be a numpy array, conversion proceeds element-wise.
    :returns: a value in Hertz. If an array was passed in, an identical sized array is returned.
    """
    return 700 * (np.exp(mel/1127.0)-1)

def rms_to_db(rms: float):
    """Root Mean Square to dB.

    Args:
        rms ([float]): root mean square

    Returns:
        float: dB
    """
    return 20.0 * math.log10(max(1e-16, rms))

def rms_to_dbfs(rms: float):
    """Root Mean Square to dBFS.
    https://fireattack.wordpress.com/2017/02/06/replaygain-loudness-normalization-and-applications/
    Audio is mix of sine wave, so 1 amp sine wave's Full scale is 0.7071, equal to -3.0103dB.

    dB = dBFS + 3.0103
    dBFS = db - 3.0103
    e.g. 0 dB = -3.0103 dBFS

    Args:
        rms ([float]): root mean square

    Returns:
        float: dBFS
    """
    return rms_to_db(rms) - 3.0103

def max_dbfs(sample_data: np.ndarray):
    """Peak dBFS based on the maximum energy sample.

    Args:
        sample_data ([np.ndarray]): float array, [-1, 1].

    Returns:
        float: dBFS
    """
    # Peak dBFS based on the maximum energy sample. Will prevent overdrive if used for normalization.
    return rms_to_dbfs(max(abs(np.min(sample_data)), abs(np.max(sample_data))))

def mean_dbfs(sample_data):
    """Peak dBFS based on the RMS energy.

    Args:
        sample_data ([np.ndarray]): float array, [-1, 1].

    Returns:
        float: dBFS
    """
    return rms_to_dbfs(
        math.sqrt(np.mean(np.square(sample_data, dtype=np.float64))))

def gain_db_to_ratio(gain_db: float):
    """dB to ratio

    Args:
        gain_db (float): gain in dB

    Returns:
        float: scale in amp
    """
    return math.pow(10.0, gain_db / 20.0)


from __future__ import division
import numpy as np
from collections import Counter
import LinearAlgebraFunctions as alg
import math

num_friends = np.random.poisson(5, 1000)
num_friends = [20 * nf_i for nf_i in num_friends]

daily_minutes = np.random.poisson(10, 1000)
daily_minutes = [15 * dm_i for dm_i in daily_minutes]

# number of points
num_points = len(num_friends)

# largest value
largest_value = max(num_friends)
smallest_value = min(num_friends)

# specific positions
sorted_values = sorted(num_friends)
smallest_value = sorted_values[0]
second_smallest_value = sorted_values[1]
second_largest_value = sorted_values[-2]

# MEASURES OF CENTRAL TENDENCY

# mean
def mean(x):
    return sum(x) / len(x)

# median
def median(v):
    """ finds the middle most value of v """
    n = len(v)
    sorted_v = sorted(v)
    midpoint = n // 2

    if n % 2 == 1:
        # if odd, return the middle value
        return sorted_v[midpoint]
    else:
        # if even, return the average of the middle values
        lo = midpoint - 1
        hi = midpoint
        return (sorted_v[lo] + sorted_v[hi]) / 2

# quantile
def quantile(x, p):
    """ returns the pth-percentile value in x """
    p_index = int(p * len(x))
    return sorted(x)[p_index]

# mode
def mode(x):
    """ returns a list, might be more than one mode """
    counts = Counter(x)
    max_count = max(counts.values())
    return [x_i for x_i, count in counts.iteritems()
        if count == max_count]

# MEASURES OF DISPERSION
# range
def data_range(x):
    return max(x) - min(x)

# variance
def de_mean(x):
    """ translate x by subtracting its mean (so the result has mean 0) """
    x_bar = mean(x)
    return [x_i - x_bar for x_i in x]

def variance(x):
    """ assumes x has at least two elements """
    n = len(x)
    deviations = de_mean(x)
    return alg.sum_of_squares(deviations) / (n - 1)

# standard deviation
def standard_deviation(x):
    return math.sqrt(variance(x))

# inter quartile range
def interquartile_range(x):
    return quantile(x, 0.75) - quantile(x, 0.25)

# CORRELATION

def covariance(x, y):
    n = len(x)
    return alg.dot(de_mean(x), de_mean(y)) / (n - 1)

def correlation(x, y):
    stdev_x = standard_deviation(x)
    stdev_y = standard_deviation(y)

    if stdev_x > 0 and stdev_y > 0:
        return covariance(x, y) / stdev_x / stdev_y
    else:
        return 0 # if no variation, correlation is 0


from numpy import mgrid, empty, float64, sqrt, array, pi

def ccp_lattice(nx,ny,nz):
  """
  creates a cubic close packed (ccp) lattice

  A CCP lattice has points at the corners and the centres of faces of a cube
  Using a unit grid the nearest neighbours are sqrt(0.5) apart

  returns positions (3,N) array
  """
  pos_base = mgrid[:nx,:ny,:nz]

  # vectors for offsets

  offset1 = array((0.5, 0.5, 0.0), dtype=float64)
  offset2 = array((0.5, 0.0, 0.5), dtype=float64)
  offset3 = array((0.0, 0.5, 0.5), dtype=float64)

  # create the 4 layers
  pos = empty((4,3,nx,ny,nz), dtype=float64)
  pos[0] = pos_base
  pos[1] = pos[0] + offset1.reshape((3,1,1,1))
  pos[2] = pos[0] + offset2.reshape((3,1,1,1))
  pos[3] = pos[0] + offset3.reshape((3,1,1,1))

  pos = pos.swapaxes(1,-1)
  pos = pos.reshape((4*nx*ny*nz,3))
  return pos

def hcp_lattice(nx,ny,nz):
  """
  creates a hexagonal close packed (hcp) lattice
  """
  pos_base = mgrid[:nx,:ny,:nz]

  # vectors for multiplies and offsets
  scales = array((1, sqrt(3), (2.0/3.0)* sqrt(6)), dtype=float64)
  offset1 = array((0.5, sqrt(3)*0.5, 0.0), dtype=float64)
  offset2 = array((0.5, sqrt(3)/6.0, sqrt(6.0)/3.0), dtype=float64)

  # create the 4 layers
  pos = empty((4,3,nx,ny,nz), dtype=float64)
  pos[0] = pos_base * scales.reshape((3,1,1,1))
  pos[1] = pos[0] + offset1.reshape((3,1,1,1))
  pos[2:] = pos[:2] + offset2.reshape((1,3,1,1,1))

  pos = pos.swapaxes(1,-1)
  pos = pos.reshape((4*nx*ny*nz,3))
  return pos, scales * (nx,ny,nz)

if __name__=='__main__':

  pos, box_size = hcp_lattice(10,10,10)
  print(box_size)
  print('number of points', pos.shape[0])
  vol = box_size[0]*box_size[1] * box_size[2]
  print('volume', vol)
  print('density', pos.shape[0] / vol)
  print('packing factor', pos.shape[0] * (pi / 6.0) / vol)

  pos = ccp_lattice(5,10,15)

  import pylab as pl
  pl.plot(pos[:,0], pos[:,2], 'bx')
  pl.show()


# -*- coding: utf-8 -*-
"""
Ejercicio 3: Integración - Fórmulas de cuadratura gaussianas.
-----------------------------------------------------------------------------
Función gauss: Halla la integral aproximada utlizando la fórmula de
Gauss-Legendre para una función f en unintervalo [a,b].

Argumentos de entrada:
    f:  función integrando (función lambda).
    a:  extremo inferior del intervalo de integración (número real).
    b:  extremo superior del intervalo de integración (número real).
    n:  número de nodos (número entero).

Argumentos de salida:
    I:  integral aproximada con la fórmula de Gauss-Legendre con n nodos de la función
        f en [a,b] (número real).

Ejemplos:
    f = lambda x : x**2
    I = gauss(f,0,2,6)
    print('Ejemplo de prueba con Gauss-Legendre =', I)
    Salida:
        Ejemplo de prueba con Gauss-Legendre = 2.666666666666666
"""

import numpy as np
import sympy as sym

# Cálculo de la integral exacta de la función f(x) = exp(x) en [0,3]
#---------------------------------------------------------------------
x = sym.Symbol('x', real=True)            # definimos la variable x simbólica
f = sym.exp(x)            # definimos la función f simbólica
I_exacta = sym.integrate(f,(x,0,3))
I_exacta = float(I_exacta)
print ('El valor exacto es: ',I_exacta)

# Función gauss
#--------------------------------------
def gauss(f,a,b,n):
    [x, w] = np.polynomial.legendre.leggauss(n)         # se obtienen los nodos x (en [-1,1]) y los pesos w
    y = ((b-a)/2.0)*x+(a+b)/2.0              # nodos en [a,b]
    I = 0.0              # fórmula de cuadratura de Gauss-Legendre
    for i in range(0,len(y)):
        I += w[i]*f(y[i])*((b-a))/2.0
    return I

#--------------------------------------
# Ejemplos
#--------------------------------------

# Ejemplo de prueba
f = lambda x : x**2
I = gauss(f,0,2,6)
print('Ejemplo de prueba con Gauss-Legendre =', I)

#--------------------------------------
# Ejercicio 3
#--------------------------------------

#-----------------------
f = lambda x :np.exp(x)
a = 0
b = 3
n = 1
I1 = gauss(f,a,b,n)
print ('El valor aproximado con 1 nodo es:', I1)
#-----------------------
n = 2
I2 = gauss(f,a,b,n)
print ('El valor aproximado con 2 nodos es:', I2)
#-----------------------
n = 3
I3 = gauss(f,a,b,n)
print ('El valor aproximado con 3 nodos es:', I3)

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


'''
Collated by Ching-Shih Tsou 鄒慶士 博士 (Ph.D.) Distinguished Prof. at the Department of Mechanical Engineering/Director at the Center of Artificial Intelligence & Data Science (機械工程系特聘教授兼人工智慧暨資料科學研究中心主任), MCUT (明志科技大學); Prof. at the Institute of Information & Decision Sciences (資訊與決策科學研究所教授), NTUB (國立臺北商業大學); the Chinese Academy of R Software (CARS) (中華R軟體學會創會理事長); the Data Science and Business Applications Association of Taiwan (DSBA) (臺灣資料科學與商業應用協會創會理事長); the Chinese Association for Quality Assessment and Evaluation (CAQAE) (中華品質評鑑協會常務監事); the Chinese Society of Quality (CSQ) (中華民國品質學會大數據品質應用委員會主任委員
Notes: This code is provided without warranty.
'''

#### Complex FFT and Interpretations
from scipy.fftpack import fft, ifft
import numpy as np
import matplotlib.pyplot as plt
np.set_printoptions(formatter={"float_kind": lambda x: "%g" % x})
fc=10 # frequency of the carrier
fs=32*fc # sampling frequency with oversampling factor=32
t=np.arange(start = 0,stop = 2,step = 1/fs) # 2 seconds duration ((2-0)/(320**(-1))=640, 2-(320**(-1))=1.996875)
x=np.cos(2*np.pi*fc*t) # time domain signal (real number)

N=256 # FFT size
X = fft(x,N) # N-point complex DFT, output contains DC at index 0
# Nyquist frequency at N/2 th index positive frequencies from
# index 2 to N/2-1 and negative frequencies from index N/2 to N-1 (Nyquist frequency included)

X[0]
abs(X[7:10])

# calculate frequency bins with FFT
df=fs/N # frequency resolution
sampleIndex = np.arange(start = 0,stop = N) # raw index for FFT plot
f=sampleIndex*df # x-axis index converted to frequencies

fig, (ax1, ax2, ax3) = plt.subplots(nrows=3, ncols=1)
ax1.plot(t,x) #plot the signal
ax1.set_title('$x[n]= cos(2 \pi 10 t)$')
ax1.set_xlabel('$t=nT_s$')
ax1.set_ylabel('$x[n]$')
ax2.stem(sampleIndex,abs(X),use_line_collection=True) # sample values on x-axis
ax2.set_title('X[k]');ax2.set_xlabel('k');ax2.set_ylabel('|X(k)|');
ax3.stem(f,abs(X),use_line_collection=True); # x-axis represent frequencies
ax3.set_title('X[f]');ax3.set_xlabel('frequencies (f)');ax3.set_ylabel('|X(f)|');
fig.show()

nyquistIndex=N//2 #// is for integer division
print(X[nyquistIndex-2:nyquistIndex+3, None]) #print array X as column
# Note that the complex numbers surrounding the Nyquist index are complex conjugates and are present at positive and negative frequencies respectively.

#### FFT Shift
from scipy.fftpack import fftshift, ifftshift
#re-order the index for emulating fftshift
sampleIndex = np.arange(start = -N//2,stop = N//2) # // for integer division
X1 = X[sampleIndex] #order frequencies without using fftShift
X2 = fftshift(X) # order frequencies by using fftshift
df=fs/N # frequency resolution
f=sampleIndex*df # x-axis index converted to frequencies
#plot ordered spectrum using the two methods
fig, (ax1, ax2) = plt.subplots(nrows=2, ncols=1)#subplots creation
ax1.stem(sampleIndex,abs(X1), use_line_collection=True)# result without fftshift
ax1.stem(sampleIndex,abs(X2),'r',use_line_collection=True) #result with fftshift
ax1.set_xlabel('k');ax1.set_ylabel('|X(k)|')
ax2.stem(f,abs(X1), use_line_collection=True)
ax2.stem(f,abs(X2),'r' , use_line_collection=True)
ax2.set_xlabel('frequencies (f)'),ax2.set_ylabel('|X(f)|');
fig.show()

#### IFFTShift
X = fft(x,N) # compute X[k]
x = ifft(X,N) # compute x[n]

X = fftshift(fft(x,N)) # take FFT and rearrange frequency order
x = ifft(ifftshift(X),N) # restore raw freq order and then take IFFT

x = np.array([0,1,2,3,4,5,6,7]) # even number of elements
fftshift(x)

ifftshift(x)

ifftshift(fftshift(x))

fftshift(ifftshift(x))

x = np.array([0,1,2,3,4,5,6,7,8]) # odd number of elements
fftshift(x)

ifftshift(x)

ifftshift(fftshift(x))

fftshift(ifftshift(x))

#### Reference:
# Viswanathan, Mathuranathan, Digital Modulations using Python, December 2019.


"""
Information Theory

This module contains functions for computing information-theoretic quantities on discrete probability distributions. Includes computation of

- Entropy
- Conditional Entropy
- KL Divergence
- Mutual Information
- Channel Capacity (using the Blahut-Arimoto algorithm)

 """

import numpy as np
from numpy import log2

eps = 1e-40

def H(p_x):
    """
    Compute the entropy of a random variable distributed ~ p(x)
    """
    return -np.sum(p_x*log2(p_x))

def H_cond(P_yx, p_x):
    """
    Compute the conditional entropy H(Y|X) given distributions p(y|x) and p(x)
    """
    return -np.sum((P_yx*log2(P_yx))@p_x)

def KL_div(p_x, q_x):
    """
    Compute the KL-divergence between two random variables
    D(p||q) = E_p[log(p(X)/q(X))]

    p_x : defines p(x), shape (dim_X, )
    q_x : defines q(x), shape (dim_X, )
    """
    if p_x.shape != q_x.shape:
        raise ValueError("p_x and q_x should have the same length")
    return np.sum(p_x * log2(p_x/q_x))

def I(Pxy):
    """
    Compute the mutual information between two random variables related by
    their joint distribution p(x,y)

    Pxy : defines the joint distribution p(x,y), shape (dim_Y, dim_X)
    """
    p_x = np.sum(Pxy, axis = 0)
    p_y = np.sum(Pxy, axis = 1)
    return KL_div(Pxy, product(p_x, p_y))

def I2(P_yx, p_x):
    """
    Compute the mutual information between two random variables given the conditional distribution
    p(y|x) and p(x)

    P_yx : matrix defining p(y|x), shape (dim_Y, dim_X)
    p_x :  defines distribution p(x), shape (dim_X,)
    """
    p_y = (P_yx@p_x).reshape(-1,1)
    Pxy = P_yx/p_y
    return np.sum( (P_yx*log2(Pxy)) @ p_x )

def product(p_x, p_y):
    """
    Compute the product distribution p(x,y) = p(x)p(y) given distributions
    p(x) and p(y)
    """
    p_x = p_x.reshape(1,-1)
    p_y = p_y.reshape(-1,1)
    Pxy = p_y@p_x
    assert Pxy.shape == (p_y.shape[0], p_x.shape[1])
    return Pxy

def blahut_arimoto(P_yx, epsilon = 0.001, deterministic = False):
    """
    Compute the channel capacity C of a channel p(y|x) using the Blahut-Arimoto algorithm. To do
    this, finds the input distribution p(x) that maximises the mutual information I(X;Y)
    determined by p(y|x) and p(x).

    P_yx : defines the channel p(y|x)
    iters : number of iterations
    """
    P_yx = P_yx + eps
    if not deterministic:
        # initialize input dist randomly
        q_x = _rand_dist((P_yx.shape[1],))
        T = 1
        while T > epsilon:
            # update PHI
            PHI_yx = (P_yx*q_x.reshape(1,-1))/(P_yx @ q_x).reshape(-1,1)
            r_x = np.exp(np.sum(P_yx*log2(PHI_yx), axis=0))
            # channel capactiy
            C = log2(np.sum(r_x))
            # check convergence
            T = np.max(log2(r_x/q_x)) - C
            # update q
            q_x = _normalize(r_x + eps)
        if C < 0:
            C = 0
        return C
    else:
        # assume all columns in channel matrix are peaked on a single state
        # log of number of reachable states
        return log2(np.sum(P_yx.sum(axis=1) > 0.999))

def _rand_dist(shape):
    """ define a random probability distribution """
    P = np.random.rand(*shape)
    return _normalize(P)

def _normalize(P):
    """ normalize probability distribution """
    s = sum(P)
    if s == 0.:
        raise ValueError("input distribution has sum zero")
    return P / s


import numpy as np

def gaussian_kernel(size, sigma=1):
    size = int(size) // 2
    x, y = np.mgrid[-size:size+1, -size:size+1]
    normal = 1 / (2.0 * np.pi * sigma**2)
    g =  np.exp(-((x**2 + y**2) / (2.0*sigma**2))) * normal
    return g

if __name__=='__main__':
    g=gaussian_kernel(5,1)
    #https://towardsdatascience.com/canny-edge-detection-step-by-step-in-python-computer-vision-b49c3a2d8123
    print(g)


# ---------------------------------------------------
# Statistical Thinking in Python (Part 1) - Thinking probalistically -- Continuous variables
# 11 fev 2021
# VNTBJR
# ---------------------------------------------------
#
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
sns.set()
# Probability density functions ------------------------------------------------
# Probability density function (PDF) - it is a mathematical description of
# the relative likelihood of observing a value of a continuos variable
# Areas under the de PDF gives the probabilities.
# Normal cumulative distribution function (Normal CDF) - it gives the
# probability the measured speed of light will be less than the value
# on the x-axis

# Introduction to the normal distribution ------------------------------------------------
# Normal distribution - it describes a continuous variable whose PDF
# has a single and simmetric peak.
# The normal distribution is parametrized by two parameters, the mean
# and the standard deviation.The mean determines where the center of
# the peak is. The standard deviation is measure of how wide the peak is,
# or how spread out the data are.

# The Normal PDF
# Draw 100000 samples from Normal distribution with stds of interest: samples_std1, samples_std3, samples_std10
samples_std1 = np.random.normal(20, 1, 100000)
samples_std3 = np.random.normal(20, 3, 100000)
samples_std10 = np.random.normal(20, 10, 100000)

# Make histograms
_ = plt.hist(samples_std1, bins = 100, density = True, histtype = 'step')
_ = plt.hist(samples_std3, bins = 100, density = True, histtype = 'step')
_ = plt.hist(samples_std10, bins = 100, density = True, histtype = 'step')

# Make a legend, set limits and show plot
_ = plt.legend(('std = 1', 'std = 3', 'std = 10'))
plt.ylim(-0.01, 0.42)
plt.show()
plt.clf()

# The Normal CDF
# Generate CDFs
x_std1, y_std1 = ecdf(samples_std1)
x_std3, y_std3 = ecdf(samples_std3)
x_std10, y_std10 = ecdf(samples_std10)

# Plot CDFs
_ = plt.plot(x_std1, y_std1, marker = '.', linestyle = 'none')
_ = plt.plot(x_std3, y_std3, marker = '.', linestyle = 'none')
_ = plt.plot(x_std10, y_std10, marker = '.', linestyle = 'none')

# Make a legend and show the plot
_ = plt.legend(('std = 1', 'std = 3', 'std = 10'), loc='lower right')
plt.show()
plt.clf()

# Thee Normal distribution: Properties and warnings ------------------------------------------------
# Are the Belmont Stakes results Normally distributed?
# Compute mean and standard deviation: mu, sigma
mean = np.mean(belmont_no_outliers)
sigma = np.std(belmont_no_outliers)

# Sample out of a normal distribution with this mu and sigma: samples
samples = np.random.normal(mean, sigma, 10000)

# Get the CDF of the samples and of the data
x_theor, y_theor = ecdf(samples)
x, y = ecdf(belmont_no_outliers)

# Plot the CDFs and show the plot
_ = plt.plot(x_theor, y_theor)
_ = plt.plot(x, y, marker='.', linestyle='none')
_ = plt.xlabel('Belmont winning time (sec.)')
_ = plt.ylabel('CDF')
plt.show()

# What are the chances of a horse matching or beating Secretariat's
# record?
# Take a million samples out of the Normal distribution: samples
samples = np.random.normal(mu, sigma, 1000000)

# Compute the fraction that are faster than 144 seconds: prob
prob = len(samples[samples >= 144])/len(samples)

# Print the result
print('Probability of besting Secretariat:', prob)

# The exponential distribution ------------------------------------------------
# The waiting time between arrivals of a Poisson process are Exponentially
# distributed. It has a single parameter: the mean waiting time.
# This distribution is not peaked.
# If you have a story you can simulate it!
def successive_poisson(tau1, tau2, size = 1):
    """Compute time for arrival of 2 successive Poisson processes."""
    # Draw samples out of first exponential distribution: t1
    t1 = np.random.exponential(tau1, size)

    # Draw samples out of second exponential distribution: t2
    t2 = np.random.exponential(tau2, size)

    return t1 + t2

# Distribution of no-hitters and cycles
# Draw samples of waiting times: waiting_times
waiting_times = successive_poisson(764, 715, 100000)

# Make the histogram
_ = plt.hist(waiting_times, bins = 100, density = True, histtype = "step")

# Label axes
_ = plt.xlabel('waiting time')
_ = plt.ylabel('PDF')

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

# Plot the CDF
x_waiting_time, y_waiting_time = ecdf(waiting_time)
_ = plt.plot(x_waiting_time, y_waiting_time, marker = '.', linestyle = 'none')
_ = plt.ylabel('CDF')
_ = plt.xlabel('waiting time')
plt.show()
plt.clf()


# -*- coding: utf-8 -*-
"""
Created on Fri Nov  3 20:01:02 2017

@author: Anastasios Tzavellas
"""
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit

def line(x, a, b):
    """
    Line equation, used
    for curve fitting algorithm

    Args:
        x: x value
         a: line coefficient
         b: line constant term

    Returns:
        The y coordinate of the point
    """
    return a*x + b

def period(L):
    """
    Calculates the theoretical period of a
    pendulum with length L. It assumes that
    the actual value of g is 9.8m/s2

    Args:
        L: The length of the pendulum

    Returns:
        The period of the pendulum in sec
    """
    g = 9.8
    return 2*np.pi*np.sqrt(L/g)

def gError(L, T, dL, dT):
    """
    Calculates the error of g
    estimation given the length, the
    period and their respective errors
    using the error propagation rule

    Args:
        L: A vector of length values
        T: A vector of period values
        dL: The error in length measurement
        dT: The error in period measurement

    Returns:
        A vector with g error values
    """
    dg = np.power(2*np.pi, 2) * np.sqrt( np.power(dL/(T*T), 2) + np.power(2*L*dT/np.power(T, 3), 2) )
    return dg

def experiment(L, T, dL, dT, dLsystm = 0):
    """
    Performs a g-measurement experiment

    Args:
        L: A vector of length measurements of the pendulum
        T: A vector of period measurements of the pendulum
        dL: The error in length measurement
        dT: The error in period measurement
        dLsystm: Systematic error of length measurement, default value 0

    Returns:
        A dictionary with the mean values of g,
        the g-error values and the measured period
        values, for each length
    """
    L = L + dLsystm             # Add systematic error, if it exists
    g = np.power(2*np.pi, 2) * L / np.power(T, 2) # Indirect g measurement from
                                                  # length and period
    dg = gError(L, T, dL, dT)   # g measurement error
    gMean = np.sum(g)/g.size    # Mean value of g measurements
    dgMean = np.sqrt(np.sum(dg*dg))/dg.size # Error of mean value of g
    return {'g':gMean, 'dg':dgMean}

def fit(experiment, L, T, dLsystm = 0):
    """
    Performs Least Square Fit on the given experiment

    Args:
        experiment: The experiment to perform LSF
        L: A vector of length measurements of the pendulum
        T: A vector of period measurements of the pendulum
        dLsystm: Systematic error of length measurement, default value 0

    Returns:
        A dictionary with the LSF value of g, the
        LSF coefficients, and the values used for the fit
    """
    x = np.power(T, 2)
    y = L + dLsystm
    result = curve_fit(line, x, y)  # y = A + Bx
    A = result[0][1]
    B = result[0][0]
    dBA = np.sqrt(np.diag(result[1]))
    g = np.power(2*np.pi, 2) * B    # Coefficient A gives g: A = (2*pi)^2 / g
    dg = np.power(2*np.pi, 2)*dBA[0]# Error of g is using error propagation rule
    return {'g':g, 'dg':dg, 'A':A, 'B':B, 'x':x, 'y':y}

gTheory = 9.8
L = np.array([0.2, 0.4, 0.8, 1.0])
dL = 0.01
dT = 0.1
noise = np.random.standard_normal(L.size) * dT
T = period(L) + noise               # Period measurements with dt=0.1s accuracy

print("Experiment without systematic error")
experiment1 = experiment(L, T, dL, dT)          # Perform experiment 1
print("Mean Value Method")
print("-----------------")
print("g = {:.2f} +- {:.2f}".format(experiment1['g'], experiment1['dg']))
print("\nLeast Squares Fit Method")
print("------------------------")
lsq1 = fit(experiment1, L, T)                   # Perform LSF for experiment 1
print("g = {:.2f} +- {:.2f}".format(lsq1['g'], lsq1['dg']))
xn = lsq1['x']
yn = np.polyval([lsq1['B'], lsq1['A']], xn)
plt.plot(xn, yn, 'r', label='$\delta L_{system} = 0$') # Plot least square line
plt.errorbar(lsq1['x'], lsq1['y'], xerr=dL, yerr=dT, fmt='r.') # Plot measurements
plt.xlabel('$T^2[sec^2]$')
plt.ylabel('$L[m]$')

dLsystm = 0.05
T = period(L+dLsystm) + noise
T = period(L+dLsystm) + np.random.standard_normal(L.size) * dT
print("\n\nExperiment with systematic error={:.2f}".format(dLsystm))
experiment2 = experiment(L, T, dL, dT, dLsystm) # Perform experiment 2,dL = 0.05
print("Mean Value Method")
print("-----------------")
print("g = {:.2f} +- {:.2f}".format(experiment2['g'], experiment2['dg']))
print("\nLeast Squares Fit Method")
print("-------------------------")
lsq2 = fit(experiment2, L, T, dLsystm)
print("g = {:.2f} +- {:.2f}".format(lsq2['g'], lsq2['dg']))
xn = lsq2['x']
yn = np.polyval([lsq2['B'], lsq2['A']], xn)
plt.plot(xn, yn, 'b', label='$\delta L_{system} = 0.05$') # Plot least square line
plt.errorbar(lsq2['x'], lsq2['y'], xerr=dL, yerr=dT, fmt='b*') # Plot measurements
plt.legend()
plt.show()


import numpy as np
from homography_dlt import direct_linear_transform

def transfer_error(pt1, pt2, H):
  """
  Calculate the transfer error between the two

  NOTE: There are other distance metrics (e.g. symmetric distance, could they be better?)

  Input:
    pt1 - Destination point
    pt2 - Point to transform onto destination plane
    H - homography to transform

  Output: Sum squared distance error
  """

  pt1 = np.append(pt1, [1])
  pt2 = np.append(pt2, [1])

  pt1_projected = ((H @ pt1).T).T

  diff = (pt2 - pt1_projected)

  sse = np.sum(diff**2)

  return sse

def homography_ransac(pts1, pts2, threshold, n_iterations):
  """
  Find a best guess for the homography to map pts2 onto
  the plane of pts1

  Input:
    pts1 - Destination plane
    pts2 - Points to transform onto destination plane#

  Output: Tuple of (Homography projecting pts2 onto pts1 plane, RANSAC inlier kps)
  """

  # Store maxInliners are points, if there is a tie in max,
  # take the H that has maxInliners with the smallest standard deviation
  maxInliers = []
  bestH = None

  for i in range(n_iterations):
    # 4 random point indexes
    random_pts_idxs = np.random.choice(len(pts1), 4)

    # Get random sample using random indexes
    pts1_sample = pts1[random_pts_idxs]
    pts2_sample = pts2[random_pts_idxs]

    # Compute H using DLT
    H = direct_linear_transform(pts1_sample, pts2_sample)

    inliers = []

    # For each correspondence
    for i in range(len(pts1)):
      # Get distance for each correspondance
      distance = transfer_error(pts1[i], pts2[i], H)

      # Add correspondence to inliners if distance less than threshold
      if (distance < threshold):
        inliers.append([pts1[i], pts2[i]])

    # If inliers > maxInliers, set as new best H
    if (len(inliers) > len(maxInliers)):
      maxInliers = inliers
      bestH = H
      # TODO: else if inliers == maxInliers, pick best H based on smallest standard deviation

  return bestH, maxInliers


def dispersion_relation_2D(p, theta, C):
    arg = C*sqrt(sin(p*cos(theta))**2 +
                 sin(p*sin(theta))**2)
    c_frac = 2./(C*p)*arcsin(arg)

    return c_frac

import numpy as np
from numpy import \
     cos, sin, arcsin, sqrt, pi  # for nicer math formulas

r = p = np.linspace(0.001, pi/2, 101)
theta = np.linspace(0, 2*pi, 51)
r, theta = np.meshgrid(r, theta)

# Make 2x2 filled contour plots for 4 values of C
import matplotlib.pyplot as plt
C_max = 1/sqrt(2)
C = [[C_max, 0.9*C_max], [0.5*C_max, 0.2*C_max]]
fix, axes = plt.subplots(2, 2, subplot_kw=dict(polar=True))
for row in range(2):
    for column in range(2):
        error = 1 - dispersion_relation_2D(
            p, theta, C[row][column])
        print error.min(), error.max()
        # use vmin=error.min(), vmax=error.max()
        cax = axes[row][column].contourf(
            theta, r, error, 50, vmin=-1, vmax=-0.28)
        axes[row][column].set_xticks([])
        axes[row][column].set_yticks([])

# Add colorbar to the last plot
cbar = plt.colorbar(cax)
cbar.ax.set_ylabel('error in wave velocity')
plt.savefig('disprel2D.png');  plt.savefig('disprel2D.pdf')
plt.show()

# See
# http://blog.rtwilson.com/producing-polar-contour-plots-with-matplotlib/
# for polar plotting in matplotlib


import numpy as np

def seidel(A, b, x):
  size = len(A)
  for j in range(size):
    d = b[j]

    for i in range(size):
      if j != i:
        d -= A[j][i] * x[i]
    x[j] = d / A[j][j]
  return x

def seidelIterations(A, b, eps, maxIterations):
  size = len(A)
  x = np.zeros(size)

  for i in range(maxIterations):
    xPrev = np.copy(x)
    x = seidel(A, b, x)

    print(f"{i}-th iteration:")
    print(x)
    if np.linalg.norm(np.dot(A, x) - b) <= eps * np.linalg.norm(b):
      print("^^^^^^^^^^^^^^")
      print(">> Малость невязки << Solution found")
      return
    if np.linalg.norm(x - xPrev) <= eps * np.linalg.norm(xPrev):
      print("^^^^^^^^^^^^^^")
      print(">> Малость нормы приближений << Solution found")
      return

  print("Number of iterations exceeded")


import sys
import csv
import math

import numpy as np
import pandas as pd

'''
Prints the information gain values for each partition (or column).
'''
def runEntropy(df):

    # Get Y and labels:
    labels = list(df.axes[1])
    y_label = labels[0]
    Y = df[y_label]
    del labels[0]

    #Get information gain and loss for each partition:
    data_str = "Partition\t\tInformation Gain"
    for i in labels:
        data_str += "\n{}:\t\t\t{}".format(i, infoGainLoss(df[i],Y))

    #Print data
    print(data_str)

'''
Computes the entropy of the Y column.
'''
def getSetEntropy(Y):
    partitionEntropy = 0
    p_1 = sum(Y)/len(Y)
    p_2 = 1-p_1
    if p_2 == 0:
        partitionEntropy = 0
    elif p_1 == 0:
        partitionEntropy = 1
    else:
        partitionEntropy = -((p_1*math.log(p_1,2)) + (p_2*math.log(p_2,2)))
    return(partitionEntropy)

'''
Computes the information gain, which is simply the entropy of the set minus
the combined entropies of each result in a partition (or a column).
'''
def infoGainLoss(X,Y):
    setEntropy = getSetEntropy(Y)
    part_dict = getPartitionEntropy(X,Y)
    set_xresults = set(X)
    part_fracs = set()
    for p in set_xresults:
        total = 0
        numPart = 0
        for i in range(len(X)):
            total += 1
            if X[i] == p:
                numPart += 1
        try:
            if not math.isnan(float(p)):
                part_fracs.add((numPart/total)*part_dict[p])
        except:
            part_fracs.add((numPart/total)*part_dict[p])
    return(setEntropy - sum(part_fracs))

'''
Gets Entropy for each value in a partitioned column and returns
them in a dictionary used to compute the combined partition entropy
in infoGainLoss.
'''
def getPartitionEntropy(X,Y):
    set_xresults = set(X)
    partitionEntropy = 0
    part_dict = {}
    for p in set_xresults:
        numTotal = 0
        numCorrect = 0
        for i in range(len(X)):
            if X[i] == p:
                numTotal += 1
                if Y[i] == 1:
                    numCorrect += 1
        if numTotal == 0:
            p_1 = 0
            p_2 = 1
        else:
            p_1 = numCorrect/numTotal
            p_2 = 1-p_1

        if p_2 == 0:
            partitionEntropy = 0
        elif p_1 == 0:
            partitionEntropy = 1
        else:
            partitionEntropy = -((p_1*math.log(p_1,2)) + (p_2*math.log(p_2,2)))

        part_dict[p] = partitionEntropy
    return part_dict


import numpy as np
from utils import transpose_matrix, norm

def conjugate_gradient(A, b, x0 = None, eps = None, maxIter = 1000):
    """
    Performs conjugate gradient method to the function f(x) = 1/2(A*Ax) - x*A*b
    :param A: a matrix mxn
    :param b: a column vector nx1
    :param x0: starting point, if None the 0 vector is used as default starting point
    :param eps: (optional) the accuracy in the stopping criterion
    :param maxIter:  (optional, default value 1000): the maximum number of iterations
    :return: [x, status, ite]:
    :  - x (mx1 real column vector): it solves ||gradient(f(x))|| = A*Ax - A*b = 0
    :  - status (string): a string describing the status of the algorithm at
    :    termination
    :    = 'optimal': the algorithm terminated having proven that x is an optimal solution, i.e.,
    :                 the norm of the gradient at x is less than the required threshold
    :    = 'finished': the algorithm terminated in m iterations since no threshold of accuracy is required
    :    = 'stopped': the algorithm terminated having exhausted the maximum number of iterations
    :  - ite: number of iterations executed by the algorithm
    """
    if x0 is None:
        x = np.zeros(A.shape[1])
    else:
        x = x0

    r = b - np.matmul(A, x)
    g_0 = np.matmul(transpose_matrix(A), r)
    d = g_0
    g = g_1 = g_0
    i = 1
    while True:
        if i>1:
            g_2 = g_1
            g_1 = g
            beta = -np.divide(np.square(norm(g_1)),np.square(norm(g_2)))
            d = g_1 - beta*d
            # print("Space used ", r.nbytes+d.nbytes+x.nbytes+g.nbytes+g_1.nbytes+g_2.nbytes)
        Ad = np.matmul(A, d)
        alpha = np.divide(np.square(norm(g_1)),np.square(norm(Ad)))
        x = x + alpha*d
        r = r - alpha*Ad
        g = np.matmul(transpose_matrix(A), r)
        ng = norm(g)

        if eps is None:
            # no stopping condition, we end up in m iterations or when the norm of the gradient is zero
            if not np.any(g):
                status = "optimal"
                break
            if i > A.shape[1]:
                status = "finished"
                break
        else:
            # check accuracy for stopping condition
            if ng <= eps:
                status = "optimal"
                break
            if i > maxIter:
                status = "stopped"
                break

        i = i+1

    return x, status, i-1


"""
Author: Rayla Kurosaki

File: interpolation.py

Description:
"""

import numpy as np
import sympy as sym

def lagrange(data):
    """
    Constructs the Lagrange Interpolating Polynomial that interpolates a list
    of points.

    :param data: A list of points to interpolate.
    :return: The Lagrange Interpolating Polynomial that interpolates a list
             of points.
    """

    def lagrange_poly(k, xs):
        """
        Constructs the Lagrange basis polynomial.

        :param k: The index of the list of x values to skip.
        :param xs: A list of x values.
        :return: The Lagrange basis polynomial
        """
        poly = 1
        x_var = sym.symbols('x')
        for i, x in enumerate(xs):
            if not (i == k):
                poly *= (x_var - x) / (xs[k] - x)
                pass
            pass
        return poly

    P = 0
    xs = [x for x, y in data]
    for j, (x, y) in enumerate(data):
        P += y * lagrange_poly(j, xs)
        pass
    return P

def newton_divided_differences(data):
    """
    Computes the coefficients for the interpolating polynomial that
    interpolates the set of data points.

    :param data: The list of data points to interpolate.
    :return: The coefficients for the interpolating polynomial that
             interpolates the set of data points.
    """
    xs, ys = [x for (x, y) in data], [y for (x, y) in data]
    ndd = np.zeros((len(xs), len(ys)))
    for j, y in enumerate(ys):
        ndd[j][0] = y
        pass
    for c in range(1, len(ys)):
        for r in range(len(ys) - c):
            num = ndd[r + 1][c - 1] - ndd[r][c - 1]
            denom = xs[r + c] - xs[r]
            ndd[r][c] = sym.nsimplify(num / denom)
            pass
        pass
    return ndd[0]

def ndd_polynomial(data):
    """
    Constructs the polynomial that interpolates the set of data points.

    :param data: The list of data points to interpolate.
    :return: The polynomial that interpolates the set of data points.
    """
    xs, ys = [x for (x, y) in data], [y for (x, y) in data]
    cs = newton_divided_differences(data)
    poly = 0
    for i, c in enumerate(cs):
        x = sym.symbols('x')
        expr = 1
        for j in range(i):
            expr *= x - xs[j]
            pass
        poly += sym.nsimplify(c * expr)
        pass
    return poly

def chebyshev():
    pass

def cubic_splines():
    pass

def natural_cubic_splines():
    pass

def bezier():
    pass


# We need install numpy in order to import it
import numpy as np

# input two matrices
mat1 = ([1, 6, 5], [3, 4, 8], [2, 12, 3])
mat2 = ([3, 4, 6], [5, 6, 7], [6, 56, 7])

# This will return dot product
res = np.dot(mat1, mat2)

# print resulted matrix
print(res)

# input two matrices of size n x m
matrix1 = [[12, 7, 3],
           [4, 5, 6],
           [7, 8, 9]]
matrix2 = [[5, 8, 1],
           [6, 7, 3],
           [4, 5, 9]]

res = [[0 for x in range(3)] for y in range(3)]

# explicit for loops
for i in range(len(matrix1)):
    for j in range(len(matrix2[0])):
        for k in range(len(matrix2)):

            # resulted matrix
            res[i][j] += matrix1[i][k] * matrix2[k][j]

print(res)


from __future__ import absolute_import

# -- IMPORT -- #
import math
import numpy as np

# -- AVAILABLE METRICS  -- #
availableMetrics = ['euclidean','manhattan','minkowski','cosine','jaccard']

# -- DISTANCE METRICS -- #
class Metrics():
	"""CLASS::Metrics:
		---
		Description:
		---
		>Base that allows to compute different distance operations."""
	def euclidean_distance(self,x,y):
		"""METHOD::EUCLIDEAN_DISTANCE:
			---
			Arguments:
			---
			>- x {np.array} -- array one.
			>- y {np.array} -- array two.
			Returns:
			---
			>- {np.array} -- The euclidean distance between the two arrays."""
		return np.sqrt(np.sum(np.power(np.subtract(x,y),2),axis=-1))

	def manhattan_distance(self,x,y):
		"""METHOD::MANHATTAN_DISTANCE:
			---
			Arguments:
			---
			>- x {np.array} -- array one.
			>- y {np.array} -- array two.
			Returns:
			---
			>- {np.array} -- The manhattan distance between the two arrays."""
		return np.sum(np.abs(np.subtract(x,y)),axis=-1)

	def minkowski_distance(self,x,y,pVal):
		"""METHOD::MINKOWSKI_DISTANCE:
			---
			Arguments:
			---
			>- x {np.array} -- array one.
			>- y {np.array} -- array two.
			>- pVal {int} -- p value for minkowski distance.
			Returns:
			---
			>- {np.array} -- The minkowski distance between the two arrays."""
		return self.__nth_root(np.sum(np.power(np.abs(np.subtract(x,y)),pVal),axis=-1),pVal)

	def cosine_distance(self,x,y):
		"""METHOD::COSINE_DISTANCE:
			---
			Arguments:
			---
			>- x {np.array} -- array one.
			>- y {np.array} -- array two.
			Returns:
			---
			>- {np.array} -- The cosine distance between two arrays."""
		y = y.reshape(x.shape[1],x.shape[0])
		return np.dot(x,y)/(np.linalg.norm(x)*np.linalg.norm(y))

	def jaccard_distance(self,x,y):
		"""METHOD::JACCARD_DISTANCE:
			---
			Arguments:
			---
			>- x {np.array} -- array one.
			>- y {np.array} -- array two.
			Returns:
			---
			>- {np.array} -- The jaccard distance between two arrays."""
		x = np.asarray(x, np.bool)
		y = np.asarray(y, np.bool)
		return np.double(np.bitwise_and(x, y).sum())/np.double(np.bitwise_or(x, y).sum())

	def __nth_root(self,value,nRoot):
		"""METHOD::__NTH_ROOT:
			---
			Arguments:
			---
			>- value {np.array} -- array containing the values to root.
			>- nRoot {int} -- root number.
			Returns:
			---
			>- {np.array} -- The nth_root of a array"""
		return np.round(value**(1/float(nRoot)))

	def __repr__(self):
		return '<class::Metrics -- Distance Computation>'


from abc import abstractmethod, ABC
import numpy as np

class Activation(ABC):
    """
    Base class for all activation functions
    """

    @abstractmethod
    def __call__(self, x):
        pass

    @abstractmethod
    def derivative(self, x):
        pass

class Linear(Activation):
    def __call__(self, x):
        return x

    def derivative(self, x):
        return 1

    def __str__(self):
        return "Linear"

class Sigmoid(Activation):
    def __call__(self, x):
        return 1 / (1 + np.exp(-x))

    def derivative(self, x):
        return self(x) * (1 - self(x))

    def __str__(self):
        return "Sigmoid"

class LeakyReLU(Activation):
    def __init__(self, alpha=0.01):
        self.alpha = alpha

    def __call__(self, x):
        return np.maximum(self.alpha * x, x)

    def derivative(self, x):
        if not isinstance(x, np.ndarray):
            return 0 if x < 0 else 1
        x = x.copy()
        negative_dims = x < 0
        x[negative_dims] = self.alpha
        x[~negative_dims] = 1
        return x

    def __str__(self):
        return f"Leaky ReLU - alpha={self.alpha}"

class ReLU(LeakyReLU):
    def __init__(self):
        super().__init__(alpha=0)

    def __str__(self):
        return "ReLU"

class Sin(Activation):
    def __call__(self, x):
        return np.sin(x)

    def derivative(self, x):
        return np.cos(x)

    def __str__(self):
        return "Sine"

class Cos(Activation):
    def __call__(self, x):
        return np.cos(x)

    def derivative(self, x):
        return -np.sin(x)

    def __str__(self):
        return "Cosine"

class Softmax(Activation):
    def __call__(self, x):
        exps = np.exp(x - np.max(x))
        return exps / np.sum(exps)

    def derivative(self, x):
        pass

    def __str__(self):
        return "Softmax"

class Tanh(Activation):
    def __call__(self, x):
        exp_x = np.exp(x)
        exp__x = np.exp(-x)
        return (exp_x - exp__x) / (exp_x + exp__x)

    def derivative(self, x):
        return 1 - self(x) ** 2

    def __str__(self):
        return "Tanh"

class MultiActivations(Activation):
    def __init__(self, dimensions, activations):
        self.activations = []
        neurons_per_act = int(np.ceil(dimensions / len(activations)))
        prev = 0
        for act in activations[:-1]:
            self.activations.append((act, prev, prev + neurons_per_act))
            prev += neurons_per_act
        self.activations.append((activations[-1], prev, dimensions))  # take the rest

    def __call__(self, z):
        x = z.copy()
        for act_func, s, e in self.activations:
            x[s:e] = act_func(x[s:e])
        return x

    def derivative(self, z):
        x = z.copy()
        for act_func, s, e in self.activations:
            x[s:e] = act_func.derivative(x[s:e])
        return x

    def __str__(self):
        return ", ".join([str(act) for act, _, _ in self.activations])


import numpy as np

# General
def epsilonMaquina(tipoDato):
    """ Funcion que trata de calcular el epsilon
    de la maquina mediante el algoritmo antes pre_
    sentado.
    Input:
        tipoDato := esta pensado para ser uno de
            los tipos proporcionados por la li_
            breria numpy.
    Output:
        Regresa el epsilon de la maquina calcula_
            do con el tipo de dato especificado.
    """
    epsilon = tipoDato(1.0)
    unidad = tipoDato(1.0)
    valor = unidad + epsilon

    while valor > unidad:
        epsilon = epsilon/tipoDato(2.0)
        valor = unidad + epsilon

    return epsilon*2

def epsilonFloat():
    """ Calculamos el epsilon de la maquina con
    precision de 32bits
    """
    return epsilonMaquina(np.float32)

def epsilonDouble():
    """ Calculamos el epsilon de la maquina con
    precision de 64 bits
        A pesar de que el flotante de python ya
    tiene esta precision, creo que es convenien_
    te especificarlo.
    """
    return epsilonMaquina(np.float64)

# Segunda parte
def respuesta(res):
    """ Funcion para formatear la respuesta. """
    return "iguales" if res else "diferentes"

def comparacion(epsilon):
    """ Esta funcion resivira el epsilon a eva_
    luar y el tipo de dato al que este correspon_
    de para hacer las comparaciones solicitadas
    en el ejercicio.
    Input:
        En caso de que
    Output:
        Las respuestas son procesadas por la
        funcion respuesta para obtener el for_
        mato solicitado
            True implica que son iguales
            False implica que son diferentes
    """
    tD = type(epsilon) # Para escribir menos
    print(f'Con {epsilon=} y tipo de dato = {str(tD)} se da que')

    # Comprobaciones
    print(f'{respuesta( tD(1 + epsilon )   == 1 )  =}')
    print(f'{respuesta( tD( epsilon/2 )    == 0 )  =}')
    print(f'{respuesta( tD(1 + epsilon/2 ) == 1 )  =}')
    print(f'{respuesta( tD(1 - epsilon/2 ) == 1 )  =}')
    print(f'{respuesta( tD(1 - epsilon/4 ) == 1 )  =}')
    print(f'{respuesta( tD( epsilon**2 )   == 0 )  =}')
    print(f'{respuesta(epsilon + tD(epsilon**2) == epsilon) =}')
    print(f'{respuesta(epsilon - tD(epsilon**2) == epsilon) =}')

    print('...\n')

if __name__ == '__main__':
    # Epsilons calculados
    eF = np.float32(epsilonFloat())
    eD = np.float64(epsilonDouble())

    # Imprimir en pantalla
    print(f'Se calculalron los epsilons \n\teF={eF} y \n\teF={eD} \n para 32 y 64 bits correspondientemente.\n')

    # Hacemos la comparacion para 32bits
    comparacion(eF)
    # y para 64
    comparacion(eD)


import numpy as np

from ..sampling import Sample

class IntegrationSample(Sample):

    def __init__(self, **kwargs):
        self.function_values = None

        # computed by the integration methods
        self.integral = None
        self.integral_err = None

        super().__init__(**kwargs)

class PlainMC(object):
    """ Plain Monte Carlo integration method.

    Approximate the integral as the mean of the integrand over a randomly
    selected sample (uniform probability distribution over the unit hypercube).
    """
    def __init__(self, ndim=1, name="MC Plain"):
        self.method_name = name
        self.ndim = ndim

    def __call__(self, fn, eval_count):
        """ Compute Monte Carlo estimate of ndim-dimensional integral of fn.

        The integration volume is the ndim-dimensional unit cube [0,1]^ndim.

        :param fn: A function accepting self.ndim numpy arrays,
            returning an array of the same length with the function values.
        :param eval_count: Total number of function evaluations used to
            approximate the integral.
        :return: Tuple (integral_estimate, error_estimate) where
            the error_estimate is based on the unbiased sample variance
            of the function, computed on the same sample as the integral.
            According to the central limit theorem, error_estimate approximates
            the standard deviation of the statistical (normal) distribution
            of the integral estimates.
        """
        sample = IntegrationSample()
        sample.data = np.random.random((eval_count, self.ndim))
        sample.function_values = fn(*sample.data.transpose())
        sample.integral = np.mean(sample.function_values)
        err = np.sqrt(np.var(sample.function_values) / eval_count)
        sample.integral_err = err
        return sample


import sys
import numpy as np
from scipy.stats import chi2
import matplotlib.pyplot as plt
import my_style

def plot_chi2(ndf=4):

    x_max = 1
    while(True):
        if chi2.sf(x_max, ndf) < 0.00001:
            break
        x_max += 1
    x_max = (x_max // 10) * 10

    x = np.linspace(0, x_max, 500)
    plt.plot(x, chi2.pdf(x, ndf), 'k-')
    plt.title('$\\chi^2$ distribusion ndf = {0}'.format(ndf))
    plt.xlabel("$\\chi^2$")
    plt.ylabel("Probability")
    plt.xlim(0, x_max)
    plt.ylim(0, None)
    plt.show()

if __name__ == "__main__":
    if len(sys.argv) == 1:
        plot_chi2()
    if len(sys.argv) == 2:
        plot_chi2(float(sys.argv[1]))


#Newton Raphson Methods

"""
Name - Suryabrata Das

Sem: V

College_Roll_NO: 703

Paper-code: CMSA DSE-IB

Registration No: A01-1112-117-003-2018

Examination roll_no: 2021151264

Subject: Numerical Methods (DSE-I)

"""
from sympy import *

def newton_raphson(f,f_prime,a,e):
    h = f(a)/f_prime(a)
    while abs(h) >= e:
        h = f(a)/f_prime(a)
        a = a - h
    print("The root of the equation is: ",a)
def main():
    x = Symbol('x')
    f = x**3 - x - 3
    f_prime = f.diff(x)
    print("Your function: ",f);
    print("Derivative of the function: ",f_prime)
    f = lambdify(x, f)
    f_prime = lambdify(x, f_prime)
    a = 1
    e = 10**(-1*5)
    newton_raphson(f,f_prime,a,e)
if __name__ == '__main__':
    main()


import numpy as np

# Write a function that takes as input a list of numbers, and returns
# the list of values given by the softmax function.
def softmax(L):
    e_sum = np.sum(np.exp(L))
    return np.exp(L) / e_sum


# The area of a circle is defined as πr^2.
# Estimate π to 3 decimal places using a Monte Carlo method.
# Hint: The basic equation of a circle is x^2 + y^2 = r^2.

# P(point inside circle) = πr^2 / (2r)^2 = π/4
# π = 4*P(point inside circle)

import numpy as np

def monte_carlo():
    radius = 1

    insidePoints = 0
    totalPoints = 0

    accuracy = 1e-7
    lastP = 0
    while True:
        point = np.random.rand(2)
        if point[0]**2 + point[1]**2 < radius:
            insidePoints += 1
        totalPoints += 1

        newP = insidePoints/totalPoints
        if lastP != newP:
            if abs(lastP - newP) < accuracy:
                return 4 * newP
        lastP = newP

# Driver code:
result = monte_carlo()
print("{0:0.2f}".format(result))


from __future__ import absolute_import, division, print_function, unicode_literals

import numpy as np
import matplotlib.pyplot as plt

def plot_series(time, series, format="-", start=0, end=None, label=None):
    plt.plot(time[start:end], series[start:end], format, label=label)
    plt.xlabel("Time")
    plt.ylabel("Value")
    if label:
        plt.legend(fontsize=14)
    plt.grid(True)

def trend(time, slope=0):
    return slope * time

time = np.arange(4 * 365 + 1)
baseline = 10
series = baseline + trend(time, 0.1)

plt.figure(figsize=(10, 6))
plot_series(time, series)
plt.show()

print(time)
print(series)

#generate time series with seasonal patterns
def seasonal_pattern(season_time):
	#arbitrary pattern
    return np.where(season_time < 0.4,
                    np.cos(season_time * 2 * np.pi),
                    1 / np.exp(3 * season_time))

def seasonality(time, period, amplitude=1, phase=0):
    """Repeats the same pattern at each period"""
    season_time = ((time + phase) % period) / period
    return amplitude * seasonal_pattern(season_time)

amplitude = 40
series = seasonality(time, period=365, amplitude=amplitude)

plt.figure(figsize=(10, 6))
plot_series(time, series)
plt.show()

#time series with both trend and seasonal patterns
slope = 0.05
series = baseline + trend(time, slope) + seasonality(time, period=365, amplitude=amplitude)

plt.figure(figsize=(10, 6))
plot_series(time, series)
plt.show()

def white_noise(time, noise_level=1, seed=None):
    rnd = np.random.RandomState(seed)
    return rnd.randn(len(time)) * noise_level

noise_level = 5
noise = white_noise(time, noise_level, seed=42)

plt.figure(figsize=(10, 6))
plot_series(time, noise)
plt.show()

#add white noise to the time series
series += noise

plt.figure(figsize=(10, 6))
plot_series(time, series)
plt.show()


import numpy as np
import pdb
from scipy import linalg as splinalg
# A = np.array([
    # [1, 1, -2, 1, 3, -1],
    # [2, -1, 1, 2, 1, -3],
    # [1, 3, -3, -1, 2, 1],
    # [5, 2, -1, -1, 2, 1],
    # [-3, -1, 2, 3, 1, 3],
    # [4, 3, 1, -6, -3, -2]
# ], dtype=float)
# b = np.array([4, 20, -15, -3, 16, -27], dtype=float)

A = np.array([
[8,4,4],
[2,-4,1],
[2,-1,3]
], dtype = float)
b = np.array([
80, 7, 22
], dtype=float)

# A = np.array([
# [3,-0.1,-0.2],
# [0.1,7,-0.3],
# [0.3,-0.2,10]
# ], dtype = float)
# b = np.array([
# 7.85, -19.3, 71.4
# ], dtype=float)

# Simplest version
def gauss1(A, b):
    assert A.shape[0] == len(b), "A and b must have the same length"
    dim = A.shape[0]
    x = np.zeros(dim)
    # Elimination
    for i in range(dim - 1):
        for j in range(i + 1, dim):
            c = A[j, i] / A[i, i]
            A[j, :] -= (c * A[i, :])
            b[j] -= (c * b[i])
    # Substitution
    x[-1] = b[-1] / A[-1, -1]
    for i in range(dim - 2, -1, -1):
        sum = b[i]
        for j in range(dim - 1, i - 1, -1):
            sum -= x[j] * A[i, j]
        x[i] = sum / A[i, i]
    return x

def gauss(A, b, tol, err):
    assert A.shape[0] == len(b), "A and b must have the same length"
    dim = A.shape[0]
    x = np.zeros(dim)
    pv = np.arange(0, dim, 1)
    err = 0
    # Eliminate everything but the last row (dim-1)
    for i in range(dim - 1):
        # Store the current pivot from the pivot list
        pvt = pv[i]
        # Store the value of the current pivot
        pvv = A[pvt, i]
        # Search the other row specified in the pivot list
        for k in pv:
            # Check if the other rows have larger pivot values
            val = A[k, i]
            # print("val ({0}) > pvv({1})".format(val, pvv))
            if val > pvv:
                # We found a larger row, store the value  and so we can check the others
                pvv = val
                pvt = k
        # Did we find a new pivot that is in a row below us?
        if pvt > pv[i]:
            # If we did switch the indices in the pivot list
            #print("We switched row {0} with pivot {1} for row {2} with pivot {3}".format(pv[i], A[pv[i], i], pvt, A[pvt,i]))
            tmp = pv[i]
            pv[i] = pvt
            pv[pvt] = tmp
        # print(pv)
        # Check if the current pivot is close to 0
        # if it is, break and set the error flag
        if np.abs(A[pv[i], i]) < tol:
            err = -1
            break
        # Here we actually perform the actual elimination
        for j in range(i + 1, dim):
            # print("c = {0}/{1}".format(A[pv[j], i], A[pv[i], i]))
            c = A[pv[j], i] / A[pv[i], i]
            # print(A[pv[j], i:])
            # print((c * A[pv[i], i:]))
            A[pv[j], i:] -= (c * A[pv[i], i:])
            # print(A[pv[j], :])
            b[pv[j]] -= (c * b[pv[i]])
    # print(A)
    #print(b)
    # Quit here is the system is singular
    if err == -1:
        return x
    # Now we begin back substitution by calculating the last x value
    x[-1] = b[pv[-1]] / A[pv[-1], -1]
    # Now we solve the remaining equations
    # dim-2 starts means we begin at second row from the end and go until the 0th row
    for i in range(dim - 2, -1, -1):
        # Grab the corresponding b value
        sum = b[pv[i]]
        # Now we sum from the last column (dim -1 ) to the current column (i-1)
        for j in range(dim - 1, i - 1, -1):
            sum -= x[j] * A[pv[i], j]
        x[i] = sum / A[pv[i], i]
    return x

def lu_factor(A, tol, err):
    """Returns the matrix A with the LU matrices and a pivot vector containing information on how the matrix was eliminated.
    Passing these values to to lu_solve with a b vector will solve the equation"""
    dim = A.shape[0]
    pv = np.arange(0, dim, 1)
    err = 0
    # Eliminate everything but the last row (dim-1)
    for i in range(dim - 1):
        # Store the current pivot from the pivot list
        pvt = pv[i]
        # Store the value of the current pivot
        pvv = A[pvt, i]
        # Search the other row specified in the pivot list
        for k in pv:
            # Check if the other rows have larger pivot values
            val = A[k, i]
            # print("val ({0}) > pvv({1})".format(val, pvv))
            if val > pvv:
                # We found a larger row, store the value  and so we can check the others
                pvv = val
                pvt = k
        # Did we find a new pivot?
        if pvt > pv[i]:
            # If we did switch the indices in the pivot list
            # print("We switched row {0} with pivot {1} for row {2} with pivot {3}".format(pv[i], A[pv[i], i], pvt, A[pvt,i]))
            tmp = pv[i]
            pv[i] = pvt
            pv[pvt] = tmp
        # print(pv)
        # Check if the current pivot is close to 0
        # if it is, break and set the error flag
        if np.abs(A[pv[i], i]) < tol:
            err = -1
            break
        # Here we actually perform the actual elimination
        for j in range(i + 1, dim):
            # print("c = {0}/{1}".format(A[pv[j], i], A[pv[i], i]))
            c = A[pv[j], i] / A[pv[i], i]
            # print(A[pv[j], i:])
            # print((c * A[pv[i], i:]))
            A[pv[j], i:] -= (c * A[pv[i], i:])
            # print(A[pv[j], :])
            #print("Replacing index {0},{1} with value {2} with {3}".format(pv[j], i, A[pv[j], i], c))
            A[pv[j], i] = c
    # print(A)
    # Quit here if the system is singular
    if err == -1:
        return None
    else:
        return (A, pv)

def lu_solve(A, pv, b):
    """ Solves the system Ax=b given the output from lu_factor"""
    dim = A.shape[0]
    x = np.zeros(dim)
    for i in range(dim - 1):
        for j in range(i + 1, dim):
            #All of our c's are stored in A from the output of LU factor
            c = A[pv[j], i]
            #Calculate the b vector that would result from the typical elimination procedure
            b[pv[j]] -= (c * b[pv[i]])
    #print(d)
    x[-1] = b[pv[-1]] / A[pv[-1], -1]
    # Now we solve the remaining equations, this is the same as Gaussian back substitution
    # dim-2 starts means we begin at second row from the end and go until the 0th row
    for i in range(dim - 2, -1, -1):
        # Grab the corresponding b value
        sum = b[pv[i]]
        # Now we sum from the last column (dim -1 ) to the current column (i-1)
        for j in range(dim - 1, i - 1, -1):
            sum -= x[j] * A[pv[i], j]
        x[i] = sum / A[pv[i], i]
    return x

def inv(A, tol, err):
    """We always assume square matrices"""
    dim = A.shape[0]
    A1 = np.zeros(A.shape)
    A, pvt = lu_factor(A, tol, err)
    if err == -1:
        return None
    for i in range(dim):
        b = np.zeros(dim)
        b[i] = 1
        x = lu_solve(A, pvt, b)
        A1[:, i] = np.copy(x)
    return A1

def gauss_seidel(A, b, x, tol, maxi, lam):
    """ x should contain initial guesses (can be 0)"""
    dim = A.shape[0]
    #Divide everything by each row by its diagnol element
    for i in range(dim):
        tmp = A[i,i]
        for j in range(dim):
            A[i,j] /= tmp
        b[i] /= tmp
        # print(A)
    for i in range(dim):
        acc = b[i]
        for j in range(dim):
            if i == j:
                # print("Skipping i = {0} and j = {1}".format(i, j))
                continue
            else:
                acc -= A[i, j] * x[j]
        # print("Old x = {0}, new x = {1}".format(x[i], acc))
        x[i] = acc
    for i in range(maxi):
        flag = 1
        for k in range(dim):
            acc = b[k]
            oldx = x[k]
            for j in range(dim):
                if k == j:
                    continue
                else:
                    # print('k = {0}, j={1}'.format(k, j))
                    acc -= (A[k,j] * x[j])
                    # print(acc)
            # print("Old x = {0}, new x = {1}".format(oldx, (lam * acc) + ((1-lam) * oldx)))
            x[k] = (lam * acc) + ((1-lam) * oldx)
            if flag ==1 and x[k] != 0:
                ea = abs((x[k] - oldx)/x[k]) * 100
                # print("Error is equal to {0}".format(ea))
                if ea > tol:
                    flag = 0
        if flag == 1:
            print('Breaking with ea = {0} and num iterations: {1}'.format(ea, i))
            break
    return x

e = 0
x2 = gauss(np.copy(A), np.copy(b), 0.001, e)
aa, pv = lu_factor(np.copy(A), 0.001, e)
x1 = lu_solve(aa, pv, np.copy(b))
print(np.dot(A,x2))
print(np.dot(A,x1))
x3 = gauss_seidel(np.copy(A), np.copy(b), np.zeros(A.shape[0]), 0.0001, 25, 1.03)
print(np.dot(A,x3))


import sympy as sp
from sympy import Float, Basic, Symbol

class Calculator:
    def __init__(self, float_length: int, tnp: float):
        self.float_length = float_length
        self.tnp = tnp

    def calculate_formula(self, expr: Basic, avg_values: dict[Symbol, Float]) -> Float:
        """Calculates expression with given values."""
        return expr.evalf(self.float_length, subs=avg_values)

    def calculate_avg(self, values: list[Float]) -> Float:
        """Calculates average value for given list of values."""
        return Float(sum(values) / len(values))

    def calculate_sigma(self, values: list[Float], avg_value: Float) -> Float:
        """
        Calculates sigma coefficient for calculation of measurement error.
        Fromula:
            σ = (∑ (xᵢ - <x>)²) / (n * (n-1))
        """
        n = len(values)
        return sp.sqrt(sum([(x - avg_value) ** 2 for x in values]) / (n * (n - 1)))

    def calculate_error_for_variable(
        self, measurements: list[Float], avg_value: Float
    ) -> Float:
        """
        Calculates measurements error for given values.
        Formula:
            ΔX = σ * tnp
        """
        return self.calculate_sigma(measurements, avg_value) * self.tnp

    def calculate_result_error(
        self,
        expr: Basic,
        errors: dict[Symbol, Float],
        avg_values: dict[Symbol, Float],
        vars: list[Symbol],
    ) -> Float:
        """
        Calculates indirect error for given formula and values.
        Formula:
            ∆f = √∑(∂f/∂yᵢ * ∆yᵢ)²
        """
        res_error = 0
        for var in vars:
            deriv = expr.diff(var).evalf(self.float_length, subs=avg_values)
            res_error += (deriv * errors[var]) ** 2
        res_error = sp.sqrt(res_error)
        return res_error


#!/usr/bin/python

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

## Various Helper Function to Calculate Error
def list_diff(l1, l2):
    '''
    Returns l1 - l2. [1, 2, 3] - [3, 4, 5] = [-2, -2, -2]
    '''
    return [l1[i] - l2[i] for i in xrange(len(l1))]

def list_norm(l):
    s = 0.0
    for a in l:
        s+= a**2
    return sqrt(s)

## Tri Diagonal Matrix Algorithm(a.k.a Thomas algorithm) solver
def TDMA(a, b, c, d):
    '''
    TDMA solver, a b c d can be NumPy array type or Python list type.
    refer to http://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm
    '''
    nf = len(a)     # number of equations
    ac, bc, cc, dc = map(np.array, (a, b, c, d))     # copy the array
    for it in xrange(1, nf):
        mc = ac[it]/bc[it-1]
        bc[it] = bc[it] - mc*cc[it-1]
        dc[it] = dc[it] - mc*dc[it-1]

    xc = ac
    xc[-1] = dc[-1]/bc[-1]

    for il in xrange(nf-2, -1, -1):
        xc[il] = (dc[il]-cc[il]*xc[il+1])/bc[il]

    del bc, cc, dc  # delete variables from memory
    return xc

'''
# Example:
M = [[1,2,0,0],
     [5,3,2,0],
     [0,9,3,5],
     [0,0,3,5]]

a = [0.0, 5.0, 9.0, 3.0]
c = [2.0, 2.0, 5.0, 0.0]
b = [1.0, 3.0, 3.0, 5.0]
f = [1.0, 2.0, 3.0, 4.0]

x = TDMA(a, b, c, f)
print x
'''

# Start
# y'' - 2y'-y = -2xe^x, 0 <= x <= 1, y'(0)-y(0)=1, y(1)=e(2cosh(sqrt(2)+1)
N = 50000
a = 0.0
b = 1.0
h = (b-a)/float(N)
yb = e*(2.0*cosh(sqrt(2.0)) + 1.0) # Left side type (fixed on right)
X = [a+i*h for i in xrange(N+1)]

# Ideal solution (got from Wolfram)
def _f(x):
    return exp(-sqrt(2.0)*x)*(x*exp(x*(1.0+sqrt(2.0)))+exp(x)+exp(2.0*sqrt(2.0)*x+x))

print ("N = "+str(N)+"; solving on ("+str(a)+", "+str(b)+"); h = "+str(h))

# y'' + py' + qy = f
def q(x):
    return -1.0

def p(x):
    return -2.0

def f(x):
    return -2.0*x*exp(x)

b = [-2.0+h**2*q(X[i]) for i in range(1, N, 1)]
c = [1.0+p(X[i])*h/2.0 for i in range(1, N-1, 1)]
a = [1.0-p(X[i])*h/2.0 for i in range(2, N, 1)]
f = [h**2*f(X[i]) for i in range(1, N, 1)]
#f[0] = f[0] - (1.0-p(X[1])*h/2.0)*ya # Uncomment if have left value
f[N-2] = f[N-2] - (1.0+p(X[N-1])*h/2.0)*yb # Uncomment if have right value

# Right side
#a += []
#b += []
#c += []
#f += []

# Left side
# y'(0)-y(0)=1 --> (Y[1]-Y[0])/h-Y[0]=1
a = [1.0-p(X[1])*h/2.0] + a
b = [-(1+1/h)] + b
c = [1/h] + c
f = [1.0] + f

# Bring a, b, c and f to the same size
a = [0.0] + a
c += [0.0]

# Calculate/plot ideal and approximated functions
Y = TDMA(a, b, c, f).tolist() + [yb]
Y_ = [_f(x) for x in X]
#print "y  =", Y
#print "y_ =", Y_
print "Error: ", list_norm(list_diff(Y, Y_))

plt.plot(X, Y, 'ro')
plt.plot(X, Y_, 'g')
plt.show()