'''
Utility functions for "Data Mining for Business Analytics: Concepts, Techniques, and
Applications in Python"

(c) 2019 Galit Shmueli, Peter C. Bruce, Peter Gedeck
'''
import math
import numpy as np
from sklearn.metrics import r2_score, mean_squared_error
from sklearn.metrics import confusion_matrix, accuracy_score

def adjusted_r2_score(y_true, y_pred, model):
    """ calculate adjusted R2
    Input:
        y_true: actual values
        y_pred: predicted values
        model: predictive model
    """
    n = len(y_pred)
    p = len(model.coef_)
    if p >= n - 1:
        return 0
    r2 = r2_score(y_true, y_pred)
    return 1 - (1 - r2) * (n - 1) / (n - p - 1)

def AIC_score(y_true, y_pred, model=None, df=None):
    """ calculate Akaike Information Criterion (AIC)
    Input:
        y_true: actual values
        y_pred: predicted values
        model (optional): predictive model
        df (optional): degrees of freedom of model

    One of model or df is requried
    """
    if df is None and model is None:
        raise ValueError('You need to provide either model or df')
    n = len(y_pred)
    p = len(model.coef_) + 1 if df is None else df
    resid = np.array(y_true) - np.array(y_pred)
    sse = np.sum(resid ** 2)
    constant = n + n * np.log(2 * np.pi)
    return n * math.log(sse / n) + constant + 2 * (p + 1)

def BIC_score(y_true, y_pred, model=None, df=None):
    """ calculate Schwartz's Bayesian Information Criterion (AIC)
    Input:
        y_true: actual values
        y_pred: predicted values
        model: predictive model
        df (optional): degrees of freedom of model
    """
    aic = AIC_score(y_true, y_pred, model=model, df=df)
    p = len(model.coef_) + 1 if df is None else df
    n = len(y_pred)
    return aic - 2 * (p + 1) + math.log(n) * (p + 1)

def regressionSummary(y_true, y_pred):
    """ print regression performance metrics

    Input:
        y_true: actual values
        y_pred: predicted values
    """
    y_true = _toArray(y_true)
    y_pred = _toArray(y_pred)
    y_res = y_true - y_pred
    metrics = [
        ('Mean Error (ME)', sum(y_res) / len(y_res)),
        ('Root Mean Squared Error (RMSE)', math.sqrt(mean_squared_error(y_true, y_pred))),
        ('Mean Absolute Error (MAE)', sum(abs(y_res)) / len(y_res)),
    ]
    if all(yt != 0 for yt in y_true):
        metrics.extend([
            ('Mean Percentage Error (MPE)', 100 * sum(y_res / y_true) / len(y_res)),
            ('Mean Absolute Percentage Error (MAPE)', 100 * sum(abs(y_res / y_true) / len(y_res))),
        ])
    fmt1 = '{{:>{}}} : {{:.4f}}'.format(max(len(m[0]) for m in metrics))
    print('\nRegression statistics\n')
    for metric, value in metrics:
        print(fmt1.format(metric, value))

def _toArray(y):
    y = np.asarray(y)
    if len(y.shape) == 2 and y.shape[1] == 1:
        y = y.ravel()
    return y

def classificationSummary(y_true, y_pred, class_names=None):
    """ Print a summary of classification performance

    Input:
        y_true: actual values
        y_pred: predicted values
        class_names (optional): list of class names
    """
    confusionMatrix = confusion_matrix(y_true, y_pred)
    accuracy = accuracy_score(y_true, y_pred)

    print('Confusion Matrix (Accuracy {:.4f})\n'.format(accuracy))

    # Pretty-print confusion matrix
    cm = confusionMatrix

    labels = class_names
    if labels is None:
        labels = [str(i) for i in range(len(cm))]

    # Convert the confusion matrix and labels to strings
    cm = [[str(i) for i in row] for row in cm]
    labels = [str(i) for i in labels]

    # Determine the width for the first label column and the individual cells
    prediction = 'Prediction'
    actual = 'Actual'
    labelWidth = max(len(s) for s in labels)
    cmWidth = max(max(len(s) for row in cm for s in row), labelWidth) + 1
    labelWidth = max(labelWidth, len(actual))

    # Construct the format statements
    fmt1 = '{{:>{}}}'.format(labelWidth)
    fmt2 = '{{:>{}}}'.format(cmWidth) * len(labels)

    # And print the confusion matrix
    print(fmt1.format(' ') + ' ' + prediction)
    print(fmt1.format(actual), end='')
    print(fmt2.format(*labels))

    for cls, row in zip(labels, cm):
        print(fmt1.format(cls), end='')
        print(fmt2.format(*row))


"""
    Code to simulate the Monty Hall Problem (MHP) (https://en.wikipedia.org/wiki/Monty_Hall_problem) and verify winning probabilities
    Creator: Aditya Basu

    Run the code with: python3 MHP.py <NUMBER OF SIMULATIONS>
    Recommended NUMBER OF SIMULATIONS: 2500-3000 (although the more the merrier)
    Approximate run-time for 2500-3000 simulations: 1 minute

    We also verify the following facts (using a plot):
    (i) probability of winning if you switch = 2/3
    (ii) probability of winning if you stick = 1/3

    Mathematical insight:
    Assume (#winning ratio after switching) as a Bernoulli random variable with p = 2/3
    or,(#winning ratio after sticking) as a Bernoulli random variable with p = 1/3
    The results we get are in accordance with the Law of Large Numbers
    i.e with very high number of simulations, the winning ratio does converge to its expected value
"""

from numpy import random
import matplotlib.pyplot as plt
from tqdm import tqdm
import sys

def DidYouWin(prizeIn: int, yourChoice: int, switch: bool):
    """
        Returns: 1 if you win the game
        Returns: 0 if you lose the game
    """
    # list of all doors
    ALL = list(range(1, 4))
    # canShow contains the doors which can be shown
    canShow = [num for num in ALL if (num != prizeIn) and (num != yourChoice)]
    show = random.choice(canShow)  # Door which is shown
    # If you switch (choose the other door)
    if switch:
        newChoice = [num for num in ALL if (num != show) and (num != yourChoice)][0]
        return int(newChoice == prizeIn)
    # If you stick (remain with the same door)
    else:
        return int(yourChoice == prizeIn)

def simulate_switch(num_sims: int):
    """
        Runs the simulation <num_sims> times where we always switch
        Returns: the ratio of simulations where we won after switching
    """
    # Running the simulation and counting the number of wins after switching
    won: int = 0
    for i in range(num_sims):
        # The door which contains the prize
        prizeIn: int = random.randint(1, 4)
        # You choose a door
        yourChoice: int = random.randint(1, 4)
        won += DidYouWin(prizeIn, yourChoice, True)
    win_ratio = won/num_sims
    return win_ratio

def simulate_stick(num_sims: int):
    """
        Runs the simulation <num_sims> times where we always stick
        Returns: the ratio of simulations where we won after sticking
    """
    # Running the simulation and counting the number of wins after sticking
    won: int = 0
    for i in range(num_sims):
        # The door which contains the prize
        prizeIn: int = random.randint(1, 4)
        # You choose a door
        yourChoice: int = random.randint(1, 4)
        won += DidYouWin(prizeIn, yourChoice, False)
    win_ratio = won/num_sims
    return win_ratio

if __name__ == "__main__":
    assert len(sys.argv) == 2, "Run with python3 MHP.py <NUMBER OF SIMULATIONS>"

    num_sims = int(sys.argv[1])
    switch = dict()
    stick = dict()
    # Simulating
    for i in tqdm(range(1, num_sims+1)):
        switch[i] = simulate_switch(i)
        stick[i] = simulate_stick(i)
    # Plotting
    keys = list(range(1, num_sims+1))
    switchlist = list(switch.values())
    sticklist = list(stick.values())

    plt.figure(figsize=(20, 6))
    plt.plot(keys, switchlist, label = "After switching")
    plt.plot(keys, sticklist, label = "After sticking")
    plt.axhline(y = 1/3, color = 'k', linestyle = '-')
    plt.axhline(y = 2/3, color = 'k', linestyle = '-')
    plt.ylabel("Winning probability",fontweight = 'bold', fontsize = 14, fontname = 'Arial')
    plt.xlabel("Number of simulations",fontweight = 'bold', fontsize = 14, fontname = 'Arial')
    plt.suptitle("Monty Hall Problem - Winning Probability Convergence", fontweight = 'bold', fontsize = 20, fontname = 'Arial')
    plt.legend(loc = 'upper right')
    plt.show()


import numpy as np

def Distance(X,Y):
    i = 0
    total = 0
    while(i < len(X)):
        total = total + pow((X[i] - Y[i]),2)
        i = i + 1
    return total**2

def KNN_test(X_train,Y_train,X_test,Y_test,K):
    i = 0
    all_distances = []
    #This gets all the distances and stores it into all_distances
    while (i < len(X_test)):
        j = 0
        distances = []
        while (j < len(X_train)):
            distances.append([Distance(X_train[j],X_test[i]),j])
            j = j + 1
        distances.sort()
        all_distances.append(distances[0:K])
        i = i + 1

    i = 0
    all_labels = []
    #This gets all the labels and puts them in all_labels
    while(i < len(X_test)):
        j = 0
        label = 0
        while (j <  K):
            label = label  + Y_train[all_distances[i][j][1]]
            j = j +1
        if(label != 0):
            label = label/(abs(label))
            all_labels.append(label)
        else :
            all_labels.append(1)
        i = i + 1

    i = 0
    num_Correct = 0
    while(i < len(X_test)):
        if (all_labels[i] == Y_test[i] ):
            num_Correct = num_Correct + 1
        i = i + 1
    accuracy = num_Correct/(len(X_test))
    return accuracy

def choose_K(X_train,Y_train,X_val,Y_val):
    best_acc = 0
    k = 0
    i = 1
    while(i < len(X_train)):
        acc = KNN_test(X_train,Y_train,X_val,Y_val, i)
        if(acc > best_acc):
            best_acc = acc
            k = i
        i = i + 2
    return k, best_acc

if __name__ == '__main__':

  X_train = np.array([[1,5], [2,6], [2,7], [3,7], [3,8], [4,8],[5,1], [5,9], [6,2], [7,2], [7,3], [8,3], [8,4], [9,5]])

  Y_train = np.array([[-1],[-1],[1],[-1],[1],[-1],[1],[-1],[1],[-1], [1],[-1], [1],[1]])

  X_test = np.array([[1,1], [2,1], [0,10], [10,10], [5,5], [3,10], [9,4], [6,2], [2,2], [8,7]])
  Y_test = np.array([[1], [-1], [1], [-1], [1], [-1], [1], [-1], [1], [-1]])
  # print(Y_test)

  # acc = KNN_test(X_train,Y_train,X_test,Y_test,5)
  # print("The accuracy is: ", acc*100, "%")
  K1, acc1 = choose_K(X_train,Y_train, X_test, Y_test)
  print("For this data, the best K is: ", K1, "with accuracy ", acc1*100, "%")


import math
import warnings

import numpy
import numpy as np

def haar_transform(x: numpy.ndarray, h: numpy.ndarray = None, orthogonal=True, shape_conform_mode='rpad', shape_conform_pad_value='median'):
    """
    Perform 1-d discrete Haar wavelet transform.
    :param x: input signal with shape (n_signals, n_samples_per_signal)
    :param h: (optional) Haar transformation matrix. Provide cached matrix for performance.
    :param orthogonal: orthogonal transformation is likely slower (default True)
    :param shape_conform_mode: how to handle data lengths that aren't powers of two. Can be 'rtrim', 'ltrim', 'lrtrim', 'rpad', 'lpad', 'lrpad' (default 'rpad')
    :param shape_conform_pad_value: what value to use if shape_conform_mode is '*pad'. Can be 'median', 'mean', or 'constant' (use zeros). (default 'median')
    :return:
        y - transformed data
        h - haar matrix used for transformation
    """

    x = np.asarray(x, dtype=np.float64)

    n_samples = x.shape[1]

    # Ensure n_samples is power of two by padding data
    if not _is_power_of_two(n_samples):
        x = _conform_shape_to_pow2(x, axis=1, mode=shape_conform_mode, pad_with=shape_conform_pad_value)
        n_samples = x.shape[1]

    # Calculate Haar matrix
    if h is None:
        h = haar_matrix(n_samples, orthogonal)

    y = x @ h.T

    return y, h

def haar_matrix(n=2, orthogonal=True):
    """
    Creates an n x n (optionally orthonormal) Haar transformation matrix. Where n is a power of 2.

    k = log2(n)

    H(k=1) = [[1, 1]
              [1, -1]]

    H(k+1) = [H(k) kron [1, 1]
              2**(k/2) * I(2**k) kron [1, -1]]

    For an orthogonal matrix, H(k) then needs to be normalized by column Euclidean norms.

    Otherwise no normalization is needed and the scaling term 2**(k/2) can be omitted.
    This is equivalent to omitting division by sqrt(2) after calculating pairwise sums and diffs.

    From:
        Steeb, W. H., Hardy, Y., & Stoop, R. (2003). Discrete wavelets and perturbation theory.
        JOURNAL OF PHYSICS-LONDON-A MATHEMATICAL AND GENERAL, 36(24), 6807-6812.
    :param n: matrix size, expected to be power of 2. Otherwise n = 2**np.floor(np.log2(n))
    :param orthogonal: True to request an orthogonal Haar matrix (slower). (default True)
    :return:
    nxn Haar matrix.
    """
    k = int(math.log2(n))
    h = _haar_matrix_recursive(k, orthogonal)

    # Normalize columns (divide by column Euclidean norm)
    if orthogonal:
        h /= np.linalg.norm(h, axis=0)
    return h

def _haar_matrix_recursive(k, orthogonal=True):
    if k > 1:
        h = _haar_matrix_recursive(k - 1, orthogonal)
        # Calculate the top and bottom parts
        h_t = np.kron(h, [1, 1])
        h_b = np.kron(np.eye(2 ** (k - 1)), [1, -1])
        # The orthogonal Haar matrix requires scaling the bottom half
        if orthogonal:
            h_b *= 2 ** ((k - 1) / 2)
        h = np.vstack((h_t, h_b))
    else:
        h = np.asarray([[1, 1], [1, -1]])

    return h

def _is_power_of_two(n: int):
    return n != 0 and (n & (n - 1) == 0)

def _conform_shape_to_pow2(data: np.ndarray, axis=0, mode='lrtrim', pad_with='median'):
    """
    Ensure data length is power of two along a specific axis.

    :param data: input data
    :param axis: (default 0)
    :param mode: 'rtrim', 'ltrim', 'lrtrim', 'rpad', 'lpad', 'lrpad' (default 'lrtrim')
    :param pad_with: 'median', 'mean', or 'constant' (use zeros). See numpy.pad() (default 'median')
    :return:
    View of sliced array (trim mode) or copy of padded array (pad mode)
    """

    data_length = data.shape[axis]
    if data_length <= 1:
        raise ValueError(f"Data length expected to be 2 or higher, is {data_length}")
    elif _is_power_of_two(data_length):
        return data
    else:
        if mode in ('rtrim', 'ltrim', 'lrtrim'):
            desired_length = 2 ** math.floor(math.log2(data_length))
        elif mode in ('lpad', 'rpad', 'lrpad'):
            desired_length = 2 ** math.ceil(math.log2(data_length))
        else:
            raise ValueError(
                f"Unrecognized mode: '{mode}', expected 'rtrim', 'ltrim', 'lrtrim', 'lpad', 'rpad', or 'lrpad'")

    if mode == 'rtrim':
        data = data.take(range(0, desired_length), axis=axis)
    elif mode == 'ltrim':
        data = data.take(range(data_length - desired_length, data_length), axis=axis)
    elif mode == 'lrtrim':
        n_ltrim = (data_length - desired_length) // 2
        n_rtrim = data_length - desired_length - n_ltrim
        data = data.take(range(n_ltrim, data_length - n_rtrim), axis=axis)
    elif mode == 'rpad':
        pw = [(0, 0)] * data.ndim
        pw[axis] = (0, desired_length - data_length)
        data = np.pad(data, pad_width=pw, mode=pad_with)
    elif mode == 'lpad':
        pw = [(0, 0)] * data.ndim
        pw[axis] = (desired_length - data_length, 0)
        data = np.pad(data, pad_width=pw, mode=pad_with)
    elif mode == 'lrpad':
        n_lpad = (desired_length - data_length) // 2
        n_rpad = desired_length - data_length - n_lpad
        pw = [(0, 0)] * data.ndim
        pw[axis] = (n_lpad, n_rpad)
        data = np.pad(data, pad_width=pw, mode=pad_with)

    assert data.shape[axis] == desired_length
    return data


from sage.all import var, sin, Matrix

from ..riemannian import RiemChart

def sphere_polar(n: int, variables: list=None) -> RiemChart:
    """
    :param n: dimension
    :param variables: Coordinates to use, optional, defaults to r, t1, t2, ... t{n-1}
    :return: a Riemmanian chart for a round sphere in polar coordinates
    :raises IndexError: if the provided list of variables has wrong length

    Create a Riemannian Chart for an n-dimensional radius 1 sphere in polar coordinates,
    misses a codimension 2 totally geodesic sphere
    """

    if variables:
        if len(variables) != n:
            raise IndexError('Number of variables is not the same as the dimension')
    else:
        var_names = ['r'] + [f't{i}' for i in range(1, n)]
        variables = var(' '.join(var_names))

    metric = [[0 for _ in range(n)] for _ in range(n)]

    for i in range(n):
        gii = 1
        for j in range(i):
            gii *= sin(variables[j]) ** 2
        metric[i][i] = gii

    return RiemChart(n, variables, metric)

def sphere_conformal(n: int, variables: list=None) -> RiemChart:
    """
    :param n: dimension
    :param variables: Coordinates to use, optional, defaults to x1, x2, ...
    :return: a Riemannian chart for a unit sphere in conformal coordinates
    :raises IndexError: if the provided list of variables has wrong length

    Create a Riemannian Chart for an n-dimensional radius 1 sphere in conformal coordinates,
    misses one point on the sphere
    """

    if variables:
        if len(variables) != n:
            raise IndexError('Number of variables is not the same as the dimension')
    else:
        var_names = [f'x{i + 1}' for i in range(n)]
        variables = var(' '.join(var_names))

    # noinspection PyUnresolvedReferences
    metric = Matrix.identity(n) * (4 / (1 + sum(v ** 2 for v in variables)) ** 2)

    return RiemChart(n, variables, metric)

def hyperbolic_half_space(n: int, variables: list=None) -> RiemChart:
    """
    :param n: dimension
    :param variables: Coordinates to use, optional, defaults to x1, x2, ...
    :return: a Riemannian chart for the upper half-space model of a hyperbolic space
    :raises IndexError: if the provided list of variables has wrong length

    Create a Riemannian Chart for an n-dimensional hyperbolic space with sectional curvature -1,
    uses the half-space model, with the last coordinate positive.
    """
    if variables:
        if len(variables) != n:
            raise IndexError('Number of variables is not the same as the dimension')
    else:
        var_names = [f'x{i + 1}' for i in range(n)]
        variables = var(' '.join(var_names))

    # noinspection PyUnresolvedReferences
    metric = Matrix.identity(n) * (1 / variables[-1] ** 2)

    return RiemChart(n, variables, metric)


import numpy as np
import pandas as pd
import sys
np.set_printoptions(threshold=sys.maxsize)

def calculate_loss(x, y, w):
    y_pred = np.sign(np.matmul(x, w))
    zeros = np.zeros(shape = (len(y_pred), 1))
    pred_hadamard_label = np.multiply(-y,y_pred)
    loss = (np.sum(np.maximum(zeros, pred_hadamard_label)))/len(y)
    return loss

def online_perceptron(x_train, y_train, x_val, y_val, n_iters):
    y_train = y_train.reshape(len(y_training),1)
    weight_array = []
    training_loss_array = []
    val_loss_array = []

    w = np.zeros(shape = (x_train.shape[1], 1))

    for i in range(n_iters + 1):
        for x, y in zip(x_train, y_train):
            x = x.reshape(1, x_training.shape[1])
            y = y.reshape(1,1)
            y_pred = np.matmul(x,w)
            # print(y_pred.shape)
            if -(y_pred * y) <= 0:
                w = w + np.matmul(y,x).T
                # print(w)
        training_loss = calculate_loss(x_train, y_train, w)
        training_loss_array.append(training_loss)
        print("Iterations " + str(i)+ " training loss " + str(training_loss))

        val_loss = calculate_loss(x_val, y_val, w)
        val_loss_array.append(val_loss)
        print("Iterations " + str(i)+ " validation loss " + str(val_loss))

        weight_array.append(w)

    return weight_array, training_loss_array, val_loss_array

x_training = np.load('/nfs/stak/users/lehoang/Downloads/CS534_I2/data/interim/x_training.npy')
y_training = np.load('/nfs/stak/users/lehoang/Downloads/CS534_I2/data/interim/y_training.npy')
x_val = np.load('/nfs/stak/users/lehoang/Downloads/CS534_I2/data/interim/x_val.npy')
y_val = np.load('/nfs/stak/users/lehoang/Downloads/CS534_I2/data/interim/y_val.npy')

n_iters = 15

# w, training_loss, val_loss = online_perceptron(x_training, y_training, x_val, y_val, n_iters)
# x_training = np.load('/nfs/stak/users/lehoang/Downloads/CS534_I2/data/interim/x_training.npy')
x_training[1]

def predict(x, w):
    y = np.sign(np.matmul(x,w))
    return y


import numpy as np
from scipy import linalg
A = np.array([[2,1],
              [3,-1],
              [0,4]])
v = np.array([-2,1])

# 1
print "Question 1"
# a
print "A.v = " + str(A.dot(v))

# b
print "{}.{} = {}".format(A[1:2,:], v, A[1:2,:].dot(v))

# c
# print A[:, 2:4].dot(v)

# 2
print ""
print "Question 2"
i2 = np.identity(2)
v2 = v
print "{}.{} = {}".format(i2, v2, i2.dot(v2))

# 3
print ""
print "Question 3"
t3 = np.array([[1,0,0],[0,1,0],[0,0,0]])
v3 = np.array([1,2,3])
print "{}.{} = {}".format(t3, v3, t3.dot(v3))

# 4
print ""
print "Question 4"
t4 = np.array([[0,1,0,0],[1,0,0,0],[0,0,1,0],[0,0,0,1]])
v4 = np.array([1,2,3,4])
print "{}.{} = {}".format(t4, v4, t4.dot(v4))

# 8
print ""
print "Question 8"
t8 = np.array([[3,0,0],[0,3,0],[0,0,3]])
v8 = np.array([2,2,2])
print "{}.{} = {}".format(t8, v8, t8.dot(v8))

# 9
print ""
print "Question 9"
t9 = np.array([[3,0,0],[0,5,0],[0,0,1]])
v9 = np.array([2,2,2])
print "{}.{} = {}".format(t9, v9, t9.dot(v9))

# 10
print ""
print "Question 10"
t10 = np.array([[0,1,0],[-1,0,0],[0,0,1]])
v10_1 = np.array([1,0,0])
v10_2 = np.array([0,1,0])
v10_3 = np.array([0,0,1])

print "{}.{} = {}".format(t10, v10_1, t10.dot(v10_1))
print "{}.{} = {}".format(t10, v10_2, t10.dot(v10_2))
print "{}.{} = {}".format(t10, v10_3, t10.dot(v10_3))
print "This rotation matrix is rotating the x and y elements by 90 degrees"

# 11
print ""
print "Question 11"
t11 = np.array([[1,0,0],
                [0,0,-1],
                [0,1,0]])
v11_1 = np.array([1,0,0])
v11_2 = np.array([0,1,0])
v11_3 = np.array([0,0,1])

print "{}.{} = {}".format(t11, v11_1, t11.dot(v11_1))
print "{}.{} = {}".format(t11, v11_2, t11.dot(v11_2))
print "{}.{} = {}".format(t11, v11_3, t11.dot(v11_3))

# 12, 13, 14, 15, 16
print ""
print "Question 12, 13, 14, 15, 16"
angleDeg12 = 30
q12 = angleDeg12 * (np.pi / 180) #30 degrees to radians
t12 = np.array([[np.cos(q12),np.sin(q12),0],[-np.sin(q12),np.cos(q12),0],[0,0,1]])
v12_1 = np.array([1,0,0])
dot12 = t12.dot(v12_1)
mag12 = (dot12[0]**2 + dot12[1]**2 + dot12[2]**2)**(1/2)
print "Angle degrees, q12 = {}".format(angleDeg12)
print "{}.{} = {}".format(t12, v12_1, dot12)
print "This rotation matrix is rotating the x and y elements by q12 degrees"
print "magnitude of vector = {} = {}".format("SQRT(a^2 + b^2 + c^2)",mag12)
print "this matrix will rotate in x, y by given number of degrees in the negative direction"

# 17
print ""
print "Question 17"
A17 = np.array([[-2,4],
                [0,3]])
B17 = np.array([[5,-3],
                [-1,-1]])
C17 = A17.dot(B17)
print "{}\t{}\t= {}".format(A17[0], B17[0], C17[0])
print "{}\t{}\t= {}".format(A17[1], B17[1], C17[1])

# 18
print ""
print "Question 18"
A18 = np.array([[-2,4],
                [0,3]])
B18 = np.array([[5,-3],
                [-1,-1]])
C18 = B18.dot(A18)
print "{}\t  {} = {}".format(B18[0], A18[0], C18[0])
print "{}\t. {}   = {}".format(B18[1], A18[1], C18[1])

# 17
print ""
print "Question 17"
A19 = np.array([[-2,4],
                [0,3]])
B19 = np.array([[5,-3],
                [-1,-1]])
C19 = np.array([[-5,-1],
                [-3,2]])
D19 = A19.dot(B19+C19)
print "{}\t   | {}\t \t{} | = {}".format(A19[0], B19[0], C19[0], D19[0])
print "{}  \t . | {}\t+\t{} | = {}".format(A19[1], B19[1], C19[1], D19[1])

# 22
print ""
print "Question 22"
angleDeg22 = 30
q22 = angleDeg22 * (np.pi / 180) #30 degrees to radians
t22 = np.array([[np.cos(q22),np.sin(q22),0],[-np.sin(q22),np.cos(q22),0],[0,0,1]])
v22_1 = np.array([1,0,0])
dot22 = t22.dot(v22_1)
print "Angle degrees, q22 = {}".format(angleDeg22)
print "{}.{} = {}".format(t22, v22_1, dot22)
print ""

angleDeg22b = -30
q22b = angleDeg22b * (np.pi / 180) #30 degrees to radians
t22b = np.array([[np.cos(q22b),np.sin(q22b),0],[-np.sin(q22b),np.cos(q22b),0],[0,0,1]])
dot22b = t22b.dot(dot22)
print "Angle degrees, q22b = {}".format(angleDeg22b)
print "{}.{} = {}".format(t22b, dot22, dot22b)
print ""

dot22c = t22.dot(t22b)
print "{}\t {} = {}".format(t22b[0], t22b[0], dot22c[0])
print "{}\t {} = {}".format(t22b[1], t22b[1], dot22c[1])
print "{}\t\t\t\t.{}\t\t\t    = {}".format(t22b[2], t22b[2], dot22c[2])

print "rotation matrix * rotation backwards = identity matrix"


import numpy as np

print(np.sum([1, 2, 3, 4, 5]))
print()

idades = np.random.randint(24, 50, size=30)
print(idades)
print()

# Media: soma todos e divide pela qtd de elementos
print(int(idades.mean()))

# Mediana: ordena a lista e pega o valor do meio.
# Se a lista conter qtd impar de elementos, pega os dois do meio, soma e divide por 2
print(np.median(idades))
print()

# Operações vetorizadas
idades1 = np.random.randint(24, 50, size=31).astype(np.int)
idades2 = np.random.randint(24, 50, size=31).astype(np.int)

diff = idades1 - idades2
print(diff)

# Media de idade da subtração de idades1 - idades2
# Sabemos que a diferença da media de idades é
print(np.round(diff.mean(), 3))
print()

# Normal: gera números aleatórios com a mesma probabilidade de aparecerem
idades3 = np.random.normal(30, 3, size=10)
print(np.round(idades3, 3))
print()

# Gera array de boleanos com considerando a regra para cada posição
array = np.random.randint(0, 50, size=30).astype(np.int)
v = array > 15
print(v)
print()

# Retorna so os itens que são true
print(array[v])
print()


"""
Suppose a casino invents a new game that you must pay $250 to play. The game
works like this: The casino draws random numbers between 0 and 1, from a
uniform distribution. It adds them together until their sum is greater than 1,
at which time it stops drawing new numbers. You get a payout of $100 each time
a new number is drawn.

For example, suppose the casino draws 0.4 and then 0.7. Since the sum is
greater than 1, it will stop after these two draws, and you receive $200. If
instead it draws 0.2, 0.3, 0.3, and then 0.6, it will stop after the fourth
draw and you will receive $400. Given the $250 entrance fee, should you play
the game?

Specifically, what is the expected value of your winnings?

From:
    http://fivethirtyeight.com/features/
        should-you-pay-250-to-play-this-casino-game
"""

import numpy as np

def trial():
    total = 0
    spins = 0
    while total < 1:
        total += np.random.random()
        spins += 1
    return spins

def main():
    n = 10000000
    dollar_return = (np.mean([trial() for _ in range(n)]))
    return_on_stake = 100 * dollar_return
    print(return_on_stake)

if __name__ == '__main__':
    main()


# Created by YongHua | 12 April 2021 23:10
# Email: yht1e20@soton.ac.uk

import numpy as np
from math import pi, sin, cos, sqrt

def stress_transformation(exx, eyy, exy, theta):
    """Stress transformation for a 2-D stress tensor matrix.

    Takes four input parameters:
    `exx` >>> the exx stress component.
    `eyy` >>> the eyy stress component.
    `exy` >>> the exy stress component.
    `theta` >>> (in degrees) which is the angle of the stress element to be rotated.

    Returns `exx'`, `eyy'` and `exy'`

    * You can choose to include or omit prefixes e.g. MPa which is 1E6, as always take
      care of units and magnitudes!
    """

    # Convert to radians for the sin and cos functions
    thetar = theta / 180 * pi

    rotation_matrix = np.array([
        [cos(thetar)**2, sin(thetar)**2, sin(2*thetar)],
        [sin(thetar)**2, cos(thetar)**2, -sin(2*thetar)],
        [-cos(thetar)*sin(thetar), cos(thetar)*sin(thetar), cos(2*thetar)]
    ])

    old_stress = np.array([exx, eyy, exy])

    new_stress = np.dot(rotation_matrix, old_stress)

    return new_stress

def find_principle(exx, eyy, exy):
    """Finds the principle values `eI` and `eII` of a 2-D stress element.
    Takes three input parameters: `exx`, `eyy` and `exy`. Returns a list of
    principal stresses"""

    invariant_1 = exx + eyy  # trace
    invariant_2 = exx * eyy - exy ** 2  # determinant

    eI = 0.5 * (invariant_1 + sqrt(invariant_1 ** 2 - 4 * invariant_2))
    eII = 0.5 * (invariant_1 - sqrt(invariant_1 ** 2 - 4 * invariant_2))

    return [eI, eII]  # returns a list

def find_maxshear(exx, eyy, exy):
    """Find the maximum shear stress `exy` in 2-D stress element subject to
    planar rotational transformation. Numerical implementation of Mohr's circle.
    Takes three input parameters: `exx`, `eyy` and `exy`. Returns a list of
    coordinates of maximum shear stresses, in the form [centre, ±radius]"""
    eI, eII = find_principle(exx, eyy, exy)
    centre = (eI + eII) / 2  # centre of Mohr's circle, 0.5tr(e)
    radius = abs(eI - centre)  # radius of Mohr's circle
    assert abs(eI - centre) == abs(eII - centre)
    maxshear = [[centre, radius], [centre, -radius]]
    return maxshear

if __name__ == '__main__':
    # example
    help(stress_transformation)
    print(stress_transformation(40, 860, 375, 30))

    help(find_principle)
    print(find_principle(-25, 75, -56))

    help(find_maxshear)
    print(find_maxshear(-60, -40, 35))


import numpy as np

def gauss_elimination(A, B, triflag=False):
    '''
    gauss_elimination.m

    Use Gauss Elimination to solve a set of linear equations A*x = b
    Uses partial pivoting to avoid singularities

    @arg
    A       - n x n numpy.ndarray
              Matrix that represents the A matrix. Must be square
    B       - n x m numpy.ndarray
              Matrix that represents the set of B vectors
    triflag - bool (optional)
              Flag to return the tridiagonal matrix of A. Default to false

    @return
    x_soln  - n x o numpy.ndarray
              Solution matrix to the system of linear equations
    A_aug   - n x n numpy.ndarray
              Upper Triangular Augmented form of input A

    @dependencies
    python 3.6.0
    numpy

    @author: Matt Marti
    @date: 2019-06-05
    '''

    # Raise Type Error if input is not a numpy array
    if type(A) != np.ndarray:
        raise TypeError("Argument 'A' is not of type numpy.ndarray");
    #
    if type(B) != np.ndarray:
        raise TypeError("Argument 'B' is not of type numpy.ndarray");
    #

    # Dimension of linear system
    n = A.shape[0]; # Number of A rows
    assert A.shape[0] == A.shape[1], "Argument A is not square matrix";
    assert B.shape[0] == A.shape[1], \
        "Argument 'B' dimension does not match dimension of argument 'A'";
    #

    # Copy A and B to local memory
    A_aug = A.copy() + 0.0; # TODO: Figure out how to convert to float
    B_aug = B.copy() + 0.0; # TODO: Figure out how to convert to float

    # Gauss Elimination Main Loop
    for i in range( 0, n-1 ): # Iterate by rows

        # Pivot zero-valued diagonal elements
        if not A_aug[i,i]:
            k = i + 1;
            successflag = False;
            for k in range( i+1, n ):
                if A[k,i]:
                    Aktemp = A_aug[k,:].copy();
                    A_aug[k,:] = A_aug[i,:];
                    A_aug[i,:] = Aktemp.copy();
                    Bktemp = B_aug[k,:].copy();
                    B_aug[k,:] = B_aug[i,:];
                    B_aug[i,:] = Bktemp.copy();
                    successflag = True;
                    break;
                #
            #
            assert successflag, 'Argument A is singular matrix';
        #

        # Perform gauss elimination on this row
        for j in range( i+1, n ): # Iterate by rows
            if A_aug[j,i]: # If there is a non-zero element
                ajioveraii = A_aug[j,i] / A_aug[i,i];
                A_aug[j,:] = A_aug[j,:] - ajioveraii * A_aug[i,:];
                B_aug[j,:] = B_aug[j,:] - ajioveraii * B_aug[i,:];
                #
            #
        #
    #

    # Back-substitution loop
    x_soln = np.zeros((n,B.shape[1]));
    for k in range( 0, B.shape[1] ):
        for i in range( n-1, -1, -1 ):
            sum = 0;
            for j in range( i+1, n ):
                sum += A_aug[i,j] * x_soln[j,k];
            #
            x_soln[i,k] = ( B_aug[i,k] - sum ) / A_aug[i,i];
        #
    #

    if triflag:
        return x_soln, A_aug;
    else:
        return x_soln;
    #
#


from manimlib.imports import *
import numpy as np
import bats
# slide scene
from manim_reveal import SlideScene
# manim interface with bats
from manimtda import *

import matplotlib

from scipy.spatial import ConvexHull

def gen_circle2(n, r=1.0):
    theta = np.linspace(0, 2*np.pi*(1 - 1./n), n)
    pts = np.array([r*np.cos(theta), r*np.sin(theta)],  dtype=np.float)
    return pts.T

def gen_point_cloud(n, scale=1.0):
	"""
	sample points on the unit square
	"""
	return np.random.uniform(low=-scale, high=scale, size=(n,2))

def get_dimension_filt(C):
	"""
	get dimension filtration data from bats complex
	"""
	spxs = []
	ts = []
	for d in range(C.maxdim() + 1):
		ts.extend([d for _ in range(C.ncells(d))])
		spxs.extend(C.get_simplices(d))

	return spxs, ts

def dimension_filtration_from_bats(C, pts, **kwargs):
	"""
	get manimtda filtration from bats filtration
	"""
	spxs, ts = get_dimension_filt(C)
	return SimplicialFiltration(pts, spxs, ts, **kwargs)

def get_convex_hull(s, pts, color=BLUE, **kwargs):
	"""
	get convex hull of pts[s] as a Manim polygon
	"""
	spts = np.array([pts[i] for i in s])
	hull_2d = ConvexHull(spts[:,:-1])
	P = Polygon(*[spts[i] for i in hull_2d.vertices],
				color=color,
				**kwargs
				)
	P.set_fill(color, opacity=0.2)
	#P.round_corners(0.05)
	#P.scale(1.3)
	return P

def get_convex_hull_barycenter(s, pts):
	"""
	get barycenter of convex hull
	"""
	spts = np.array([pts[i] for i in s])
	hull_2d = ConvexHull(spts[:,:-1])
	hull_pts = spts[hull_2d.vertices]
	return np.mean(hull_pts, axis=0)

class Nerve(SlideScene):
	CONFIG={
        "camera_config":{"background_color":"#F0F1EB"},
        "video_slides_dir":"../video_slides"
    }
	def construct(self):
		# pts = gen_circle2(20, r=2.5)
		# pts = pts + np.random.normal(scale=0.2, size=pts.shape)
		pts = gen_point_cloud(100, scale=2.5)

		# put points into bats
		pbats = bats.DataSet(bats.Matrix(pts))
		# get landmarks
		lbats = bats.greedy_landmarks(pbats, 3, bats.Euclidean(), 0)
		lms = np.array(lbats.data()) # numpy array
		# get cover based on landmarks - assign each point to nearest landmarks
		cover = bats.landmark_eps_cover(pbats, lbats, bats.LInfDist(), 0.7)

		# construct the nerve
		N = bats.Nerve(cover, 2)

		# Fbats = WeakAlphaFiltration(pts)
		Fbats = bats.RipsFiltration(pbats, bats.Euclidean(), 0.0, 2)
		RFC = bats.ReducedFilteredChainComplex(Fbats, bats.F2())

		pts = np.hstack((pts, np.zeros((pts.shape[0], 1))))
		lms = np.hstack((lms, np.zeros((lms.shape[0], 1))))
		bcs = np.array([get_convex_hull_barycenter(s, pts) for s in cover])
		print([len(s) for s in cover])
		print(bcs)

		# this is just for point cloud
		F = filtration_from_bats(Fbats, pts, color=BLACK)
		F.shift(3*RIGHT)

		# construct filtration on Nerve
		FN = dimension_filtration_from_bats(N, bcs, color=RED, tri_opacity=0.4)
		FN.shift(3*RIGHT)

		# this just generates the point cloud
		title = TexMobject(r"\text{Nerve of a Cover }\mathcal{N}(\mathcal{U})", color=BLACK).shift(3*UP)
		t = 0.0
		anim = [FadeIn(title)]
		Ft = F.step_to(t)
		anim.append(FadeIn(Ft))
		self.play(*anim)
		self.slide_break()

		# show the open sets
		line0 = TexMobject(r"\mathcal{U}", r"=\{U_0, U_1, U_2\}", color=BLACK).shift(3*LEFT + 1*UP)
		anim = [FadeIn(line0)]
		hulls = [get_convex_hull(cover[i], pts).shift(3*RIGHT) for i in range(len(cover))]
		anim.extend([FadeInFrom(h, DOWN) for h in hulls])
		self.play(*anim)
		self.slide_break()

		# now let's highlight the barycenters
		line1 = TexMobject(r"U_0, U_1, U_2 \in \mathcal{N}(\mathcal{U})_0", color=BLACK)
		line1.next_to(line0, DOWN)
		anim = [FadeIn(line1)]
		Nt = FN.step_to(0)
		anim.append(FadeIn(Nt))
		self.play(*anim)
		self.slide_break()

		# add 1 simplices of Nerve
		line2 = TexMobject(r"(U_i, U_j) \in \mathcal{N}(\mathcal{U})_1", color=BLACK)
		line2a = TexMobject(r"\text{if }~U_i \cap U_j \ne \emptyset", color=BLACK)
		line2.next_to(line0, DOWN)
		line2a.next_to(line2, DOWN)
		anim = [Transform(line1, line2), FadeIn(line2a)]
		Nt = FN.step_to(1)
		anim.append(FadeIn(Nt))
		self.play(*anim)
		self.slide_break()

		# add 2-simplices of nerve
		line3 = TexMobject(r"(U_i, U_j, U_k) \in \mathcal{N}(\mathcal{U})_2", color=BLACK)
		line3a = TexMobject(r"\text{if }~U_i \cap U_j \cap U_k \ne \emptyset", color=BLACK)
		line3.next_to(line0, DOWN)
		line3a.next_to(line3, DOWN)
		anim = [Transform(line1, line3), Transform(line2a, line3a)]
		Nt = FN.step_to(2)
		anim.append(FadeIn(Nt))
		self.play(*anim)
		self.slide_break()


import numpy as np
import matplotlib.pyplot as plt

# This function improves the solution Ax=b with one Gauss-Seifel step, with the
# result overwriting x. The matrix A is hardcoded and represents the Poisson equation.
def gaussseidel_step(x, b, N):
   #TODO: fill in your code
   #x[i,j] = ...

# This function calculates the resdiuum vector res = b - Ax, for input vectors
# of length N. The output is stored in res.
def calc_residuum(x, b, N, res):
    #TODO: fill in your code
    #res[i,j] = ...

# This function calculates the norm of the vector of length N,
# defined as the usual quadratic vector norm.
def norm_of_residual(res, N):
    sum = np.sum(res**2)
    return np.sqrt(sum)

#This function restricts the NxN mesh stored in 'fine[ ]' to the NNxNN mesh stored in 'coarse[ ]'
def do_restrict(N, fine, NN, coarse):
    #TODO: fill in your code
    #coarse[i,j] = ...

# This function interpolates the the NNxNN mesh stored in 'coarse[ ]' to NxN mesh stored in 'fine[ ]'
def do_prolong(NN, coarse, N, fine):
    #TODO: fill in your code
    #fine[i,j] = ...

# This function carries out a V-cycle using the multigrid approach, down to N=4.
def do_v_cycle(x, b, N):
    gaussseidel_step(x, b, N)

    if N > 4:
        NN = N / 2;

        #allocate some storage for the residual and error on the current and a coarsened mesh */
        res = np.zeros((N,N))
        err = np.zeros((N,N))
        res_coarse = np.zeros((NN,NN))
        err_coarse = np.zeros((NN, NN))

        #calculate the residuum
        calc_residuum(x, b, N, res)

        #restrict the residuum
        do_restrict(N, res, NN, res_coarse)

        #now multiply the residuum on the coarser mesh (our b)
        #with a factor of 4 to take care of the (h) -> (2h) change, and the fact that we do not change A
        res_coarse[:,:] *= 4

        #call the V-cycle again, recursively
        do_v_cycle(err_coarse, res_coarse, NN)

        #now interpolate the error to the finer mesh
        do_prolong(NN, err_coarse, N, err)

        #finally add the error to the current solution
        x[:,:] += err

    gaussseidel_step(x, b, N)

N = 256
steps = 2000
L = 1.0
h = L / N
eta = 0.1 * L
rho0 = 10.0

res = np.zeros((N,N))

#now set-up the density field
x = (np.arange(-N/2,N/2)+0.5)*h
mesh = np.meshgrid(x,x)
r2 = mesh[0] * mesh[0] + mesh[1] * mesh[1]

rho = rho0* np.exp(-r2/(2*eta**2))
sum = np.sum(rho)

#initialize the starting values for phi[] and b[]
rho -= sum/(N**2)
b = 4 * np.pi*h**2*rho
phi = np.zeros((N,N))

#open a file for outputting the residuum values, and then do 2000 Jacobi steps
residuals = np.zeros(steps)
f = file("res_multigrid.txt", "w")
f.write("%d\n"%steps)

for i in np.arange(steps):
    do_v_cycle(phi, b, N)

    calc_residuum(phi, b, N, res)

    r = norm_of_residual(res, N)

    print("iter=%d:  residual=%g\n"%(i, r))
    f.write("%g\n"%r)
    residuals[i] = r

f.close()

plt.semilogy(residuals, label="multigrid")
plt.show()


# A bee walks around on a honeycomb, an infinite tessalating hexagonal grid,
# starting at a fixed hexagon. At each step, the bee moves to one of the six adjacent
# hexagons with equal probability. We'll assume adjacent hexagons are always a
# distance of one unit away from each other.

# directions = [N, NE, SE, S, SW, NW]

import random
import numpy as np
import pandas as pd

def bee(steps):
    distance = 0
    change = 0

    for step in range(1, steps + 1):
        #print("step: {}".format(step))
        if distance == 0: # Movement from the starting point
            change = 1
        else: # Steps outside the starting point
            change = random.choices([-1, 0, 1], [1/6, 2/6, 3/6])[0]
        #print("change: {}".format(change))
        distance += change
        #print("distance: {}".format(distance))
        #print("\n")
    return distance

def simulation(steps, experiments, distquery, mincond):
    distances = []
    expdist = 0
    expdev = 0

    for exp in range(experiments):
        distances.append(bee(steps))
    expdist = np.mean(distances)
    devs = abs(expdist - distances)
    expdev = np.mean(devs)

    condtrues = 0
    querytrues = 0
    for distance in distances:
        if distance >= mincond:
            condtrues += 1
            if distance >= distquery:
                querytrues += 1

    if condtrues != 0:
        probquery = querytrues / condtrues

    print("Based on {} experiments, after {} steps, the expected value of the bee's distance from the starting point is {} with {} expected deviation. The probability that its distance is at least {}, given that it is at least {}, is {}."
    .format(experiments, steps, expdist, expdev, distquery, mincond, probquery))

# After 16 steps, what is the expected value of the bee's distance from the starting hexagon?
# After 16 steps, what is the expected value of the deviation of the bee's distance from the starting hexagon?

# After 64 steps, what is the expected value of the bee's distance from the starting hexagon?
# After 64 steps, what is the expected value of the deviation of the bee's distance from the starting hexagon?

simulation(16, 1000000, 8, 6)
print("\n")
simulation(64, 1000000, 24, 20)

# After 16 moves, what is the probability that the bee is at least A = 8 distance away from the starting hexagon, given it is at least B = 6 distance?
# After 16 moves, what is the probability that the bee is at least A = 24 distance away from the starting hexagon, given it is at least B = 20 distance?

CS fundamentals/Random Walk with Python/DI_random_walk_challenges/bee.py


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Program: Fit peaks with Lorentzian distribution
Version: 20201123
@author: Pranab Das (GitHub: @pranabdas)
data = suv.fit_lorentz(x, y, a='', x0='', gamma='', xmin='', xmax='')
"""

def fit_lorentz(x, y, a='', x0='', gamma='', xmin='', xmax='', num=1000):
    import numpy as np
    from scipy import optimize

    def lorentz(x, a, x0, gamma):
        return a*gamma**2/(np.pi*gamma*((x - x0)**2 + gamma**2))

    if xmin:
        xmin = float(xmin)
        xmin_id = np.where(abs(x-xmin)==min(abs(x-xmin)))[0][0]
        x = x[xmin_id:]
        y = y[xmin_id:]
    else:
        xmin = x[0]

    if xmax:
        xmax = float(xmax)
        xmax_id = np.where(abs(x-xmax)==min(abs(x-xmax)))[0][0]
        x = x[:xmax_id]
        y = y[:xmax_id]
    else:
        xmax = x[-1]

    if not a:
        a = max(y)

    if not x0:
        x0 =  x[np.where(y==max(y))][0]

    if not gamma:
        gamma = abs(x[-1] - x[0])/10

    params, params_covariance = optimize.curve_fit(lorentz, x, y, p0=[a, x0, gamma])

    print("a =", params[0],"\nx0 =", params[1], "\ngamma =", params[2], \
        "\nFWHM =", 2 * params[2])

    fit_x = np.linspace(xmin, xmax, num)
    fit_y = lorentz(fit_x, params[0], params[1], params[2])

    return fit_x, fit_y


# A Sphere, brough to you by PharaohCola13

import mpl_toolkits.mplot3d.axes3d as p3
import matplotlib.pyplot as plt
from matplotlib import *
from numpy import *
from mpl_toolkits.mplot3d.art3d import *
from matplotlib.animation import *

name = "Roman Surface"

def shape(fig, alpha, color, edge_c, edge_w, grid, sides, edges, multi_pi, figcolor, rotation, rotmagt, rotmagp, save):
	plt.clf()
    # Definition of x
	def x_(u,v):
		    x = ((sqrt(2) * cos(2*u)*cos(v)**2) + (cos(u) * sin(2*v)))/(2 - a*sqrt(2) * sin(3*u)*sin(2*v))
		    return x

	# Definition of y
	def y_(u,v):
		    y = ((sqrt(2) * sin(2*u)*cos(v)**2) - (sin(u) * sin(2*v)))/(2 - a*sqrt(2) * sin(3*u)*sin(2*v))
		    return y

	# Definition of z
	def z_(u,v):
		    z  = (3 * cos(v)**2)/(2 - a*sqrt(2) * sin(3*u)*sin(2*v))
		    return z

	a = 0 # a = 0, Roman Surface. a = 1, Boys Surface
	# Values of the angles
	s = sides
	u = linspace(0, 2 * pi, s + 1)
	v = linspace(0, pi/2, edges)

	u, v = meshgrid(u, v)

	# Symbolic Representation
	x = x_(u,v)
	y = y_(u,v)
	z = z_(u,v)

	# Figure Properties
	ax = p3.Axes3D(fig)
	ax.set_facecolor(figcolor) # Figure background turns black

	# Axis Properties
	plt.axis(grid) # Turns off the axis grid
	plt.axis('equal')

	# Surface Plot
	roman = ax.plot_surface(x, y, z)

	roman.set_alpha(alpha) # Transparency of figure
	roman.set_edgecolor(edge_c) # Edge color of the lines on the figure
	roman.set_linewidth(edge_w) # Line width of the edges
	roman.set_facecolor(color) # General color of the figure

	def rot_on():
		def animate(i):
			ax.view_init(azim=rotmagt * i, elev=rotmagp * i)

		if save == "MP4":
			# Animate
			ani = FuncAnimation(fig, animate, frames=500,
								interval=100, save_count=50)  # frames=100)#, repeat=True)

			Writer = writers['ffmpeg']
			writer = Writer(fps=30, bitrate=1800)
			ani.save('{}.mp4'.format(name), writer=writer)
		else:
			#save = None
			# Animate
			ani = FuncAnimation(fig, animate,
								interval=1, save_count=50)  # frames=100)#, repeat=True)
			pass

		plt.ion()
		plt.show()
		time.sleep(0)
		plt.close()

	if rotation == "On":
		rot_on()
	elif rotation == "Off":
		pass


# Chapter 11

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

## Review

[The previous notebook](https://colab.research.google.com/github/AllenDowney/BiteSizeBayes/blob/master/10_joint.ipynb) I introduced cross tabulation, joint distributions, conditional disrtribution, and marginal distributions.

In this notebook, we'll apply these ideas to Bayesian inference.

But first I want to introduce a computational tool we will need, outer operations.

## Outer operations

Suppose you have two sequences, like `t1` and `t2`:

t1 = [1,3,5]

t2 = [2,4]

Many useful operations can be expressed in the form of an "outer operation" of these sequences.

The most common outer operation is the outer product, which computes the product of every pair of values, one from each sequence.

For example, here is the outer product of `t1` and `t2`:

a = np.multiply.outer(t1, t2)
a

We can understand this result more easily if we put it in a DataFrame:

pd.DataFrame(a, index=t1, columns=t2)

The values from `t1` appear along the rows of the result; the values from `t2` appear along the columns.

Each element of the result is the product of an element from `t1` and an element from `t2`.

The outer sum is similar, except that each element is the *sum* of an element from `t1` and an element from `t2`.

a = np.add.outer(t1, t2)
a

pd.DataFrame(a, index=t1, columns=t2)

We can do the same thing with almost any operation.  For example, the "outer greater than" operation compares each element from `t1` to each element of `t2`; the result is an array of Boolean values.

a = np.greater.outer(t1, t2)
a

pd.DataFrame(a, index=t1, columns=t2)

These outer operations work with Python lists and tuples, and NumPy arrays, but not Pandas Series.

However, the following function works with Pandas Series, and puts the result into a DataFrame.

def outer_product(s1, s2):
    """Compute the outer product of two Series.

    First Series goes down the rows;
    second goes across the columns.

    s1: Series
    s2: Series

    return: DataFrame
    """
    a = np.multiply.outer(s1.to_numpy(), s2.to_numpy())
    return pd.DataFrame(a, index=s1.index, columns=s2.index)

It might not be obvious yet why these operations are useful, but we'll see some examples soon.

With that, we are ready to take on a new Bayesian problem.

## How tall is A?

Suppose I choose two people from the population of adult males in the U.S.; I'll call them A and B.  If we see that A taller than B, how tall is A?

To answer this question:

1. I'll use background information about the height of men in the U.S. to form a prior distribution of height,

2. I'll construct a joint distribution of height for A and B, and update it with the information that A is taller, and

3. I'll extract from the posterior joint distribution the posterior distribution of height for A.

In the U.S. the average height of male adults in 178 cm and the standard deviation is 7.7 cm.  The distribution is not exactly normal, because nothing in the real world is, but the normal distribution is a pretty good model of the actual distribution, so we can use it as a prior distribution for A and B.

Here's an array of equally-spaced values from roughly 3 standard deviations below the mean to 3 standard deviations above.

mean = 178
std = 7.7

xs = np.arange(mean-24, mean+24, 0.5)

SciPy provides a function called `norm` that represents a normal distribution with a given mean and standard deviation, and provides `pdf`, which evaluates the probability distribution function (PDF), which we will use as the prior probabilities.

from scipy.stats import norm

ps = norm(mean, std).pdf(xs)

I'll store the `xs` and `ps` in a Series that represents the prior PMF.

prior = pd.Series(ps, index=xs)

And normalize it:

prior /= prior.sum()

And here's what it looks like.

prior.plot()

plt.xlabel('Height in cm')
plt.ylabel('Probability')
plt.title('Distribution of height for men in U.S.');

We can think of this prior distribution as the marginal distribution for A and B, but what we want is the joint probability of their heights.

## Joint distribution

As we saw in the previous notebook, it is not *generally* possible to construct a joint distribution if we only have the marginals, because the marginals don't contain information about correlations between the variables.

However, in the special case where there are no correlations, or they are small enough to ignore, it *is* possible to construct the joint distribution.

To see how, let's consider one element of the joint distribution,

$P(A_y~\mathrm{and}~B_x)$

which is the probability that `A` is $y$ cm tall and `B` is $x$ cm tall.  We can rewrite this conjuction in terms of conditional probability:

$P(A_y)~P(B_x~|~A_y)$

We can compute $P(A_y)$ from the marginal distribution, but how should we compute the conditional probability, $P(B_x~|~A_y)$?

In this case, the heights of `A` and `B` are "independent", which means that knowing the height of `A` provides no additional information about the height of `B`.

And that means that the conditional probability, $P(B_x~|~A_y)$, is just the marginal probability $P(B_y)$.

Which means that in this case, the joint probability is just the product of the marginal probabilities.

$P(A_y~\mathrm{and}~B_x) = P(A_y)~P(B_x)$

Now, to compute the joint distribution, we have to compute this product for all values of $x$ and $y$.  And we can do that by computing the outer product of the marginal distributions, like this:

joint = outer_product(prior, prior)
joint.shape

If the prior is normalized, the joint prior should also be normalized.

joint.to_numpy().sum()

The following function uses `pcolormesh` to plot the joint distribution.

Recall that `outer_product` puts the values of `A` along the rows and the values of `B` across the columns.

def plot_joint(joint):
    """Plot a joint distribution.

    joint: DataFrame representing a joint PMF
    """
    plt.pcolormesh(joint.index, joint.index, joint)
    plt.ylabel('A height in cm')
    plt.xlabel('B height in cm')

And here's what the result looks like.

plot_joint(joint)
plt.colorbar()
plt.title('Joint prior distribution of height for A and B');

As you might expect, the probability is highest near the mean and drops off away from the mean.

Another way to visualize the joint distribution is a contour plot.

def plot_contour(joint):
    """Plot a joint distribution.

    joint: DataFrame representing a joint PMF
    """
    plt.contour(joint.index, joint.index, joint)
    plt.ylabel('A height in cm')
    plt.xlabel('B height in cm')

plot_contour(joint)
plt.title('Joint prior distribution of height for A and B');

Each circle represents a level of equal probability.

## Likelihood

Now that we have a joint PMF that represents the prior distribution, we can update it with the data, which is that `A` is taller than `B`.

Each element in the joint distribution represents a hypothesis about the heights of `A` and `B`; for example:

* The element `(180, 170)` represents the hypothesis that `A` is 180 cm tall and `B` is 170 cm tall.  Under this hypothesis, the probability that `A` is taller than `B` is 1.

* The element `(170, 180)` represents the hypothesis that `A` is 170 cm tall and `B` is 180 cm tall.  Under this hypothesis, the probability that `A` is taller than `B` is 0.

To compute the likelihood of every pair of values, we can extract the values from the prior, like this:

Y = prior.index.to_numpy()
X = prior.index.to_numpy()

And then apply the `outer` version of `np.greater`, which compares every element of `Y` (height of `A`) to every element of `X` (height of `B`).

a = np.greater.outer(Y, X)

The result is an array, which we can put in a DataFrame with the corresponding `index` and `columns`.

likelihood = pd.DataFrame(a, index=Y, columns=X)

Here's what it looks like:

plot_joint(likelihood)
plt.title('Likelihood of A>B');

The likelihood of the data is 1 where `Y>X` and 0 otherwise.

## The update

We have a prior, we have a likelihood, and we are ready for the update.  As usual, the unnormalized posterior is the product of the prior and the likelihood.

unnorm_posterior = joint * likelihood

And we can get the normalized posterior by dividing through by the total.

total = unnorm_posterior.to_numpy().sum()
joint_posterior = unnorm_posterior / total
total

The total probability of the data is a little less than $1/2$.

Here's what the normalized posterior looks like.

plot_joint(joint_posterior)
plt.colorbar()

plt.title('Joint posterior distribution of height for A and B');

It looks like a sunrise as seen from the deck of a [heeling sailboat](https://en.wikipedia.org/wiki/Sailing#Heeling).

## The marginals

From the posterior joint distribution we can extract the posterior marginal distribution of `A` and `B`.

def marginal(joint, axis):
    """Compute a marginal distribution.

    axis=0 returns the marginal distribution of the second variable
    axis=1 returns the marginal distribution of the first variable

    joint: DataFrame representing a joint PMF
    axis: int axis to sum along

    returns: Series representing a marginal PMF
    """
    return joint.sum(axis=axis)

marginal_A = marginal(joint_posterior, axis=1)
marginal_B = marginal(joint_posterior, axis=0)

Here's what they look like.

prior.plot(label='Prior')
marginal_A.plot(label='Posterior for A')
marginal_B.plot(label='Posterior for B')

plt.xlabel('Height in cm')
plt.ylabel('Probability')
plt.title('Prior and posterior distributions for A and B')
plt.legend();

As you might expect, the posterior distribution for `A` is shifted to the right and the posterior distribution for `B` is shifted to the left.

We can summarize the results by computing the posterior means:

def pmf_mean(pmf):
    """Compute the mean of a PMF.

    pmf: Series representing a PMF

    return: float
    """
    return np.sum(pmf.index * pmf)

pmf_mean(prior)

pmf_mean(marginal_A), pmf_mean(marginal_B)

Based on the observation that `A` is taller than `B`, we are inclined to believe that `A` is a little taller than average, and `B` is a little shorter.

Notice that the posterior distribution are a little taller and narrower than the prior.  We can quantify that my computing their standard deviations.

def pmf_std(pmf):
    """Compute the standard deviation of a PMF.

    pmf: Series representing a PMF

    return: float
    """
    deviation = pmf.index - pmf_mean(pmf)
    var = np.sum(deviation**2 * pmf)
    return np.sqrt(var)

pmf_std(prior), pmf_std(marginal_A)

The standard deviation of the posterior distributions are a little smaller, which means we are a little more certain about the heights of `A` and `B` after we compare them.

## Conditional posteriors

Now suppose we measure `B` and find that he is 185 cm tall.  What does that tell us about `A`?

We can answer that question by extracting the conditional posterior distribution for `A`, conditioned on `B=185`.

Possible heights for `A` run down the rows of the joint PMF, so each row is an unnormalized posterior distribution conditioned on `A`.

And possible heights for `B` run across the columns, so each column is an unnormalized posterior distribution conditioned on `B`.

So we can condition on `B` by selecting a column and normalizing it.

cond_A = joint_posterior[185].copy()
cond_A /= cond_A.sum()

prior.plot(label='Prior')
marginal_A.plot(label='Posterior for A')
cond_A.plot(label='Posterior for A given B=185', color='C4')

plt.xlabel('Height in cm')
plt.ylabel('Probability')
plt.title('Prior, posterior and conditional distribution for A')
plt.legend();

The posterior conditional distribution is cut off at 185 cm, because we have established that `A` is taller than `B` and `B` is 185 cm.

And the posterior conditional is substantially different from the unconditional posterior; that is, for each value of $y$

$P(A_y | B_x) \ne P(A_y)$

which means that in the posterior distribution, `A` and `B` are not independent.

## Elo rating

[The Elo rating system](https://en.wikipedia.org/wiki/Elo_rating_system) is a way to quantify the skill level of players for games like chess.

It is based on a model of the relationship between the ratings of players and the outcome of a game.  Specifically, if $R_A$ is the rating of player $A$ and $R_B$ is the rating of player $B$, the probability that $A$ beats $B$ is given by the [logistic function](https://en.wikipedia.org/wiki/Logistic_function):

$P(A~\mathrm{wins}) = 1 / (1 + 10^{(R_B-R_A)/400})$

The parameters $10$ and $400$ are arbitrary choices that determine the range of the ratings.  In chess, values range from 100 to 2800.

Notice that the probability of winning depends only on the difference in rankings.  As an example, if $R_A$ exceeds $R_B$ by 100 points, the probability that $A$ wins is

1 / (1 + 10**(-100/400))

**Exercise:** Suppose `A` has a current rating of 1600, but we are not sure it is accurate.  We could describe their true rating with a normal distribution with mean 1600 and standard deviation 100, to indicate our uncertainty.

And suppose `B` has a current rating of 1800, with the same level of uncertaintly.

Finally, `A` and `B` play and `A` wins.  How should we update their ratings?

To answer this question:

1. Construct prior distributions for `A` and `B`.

2. Use them to construct a joint distribution, assuming that the prior distributions are independent.

3. Use the logistic function above to compute the likelihood of the outcome under each joint hypothesis.  Hint: use `np.subtract.outer`.

4. Use the joint prior and likelihood to compute the joint posterior.

5. Extract and plot the marginal posteriors for `A` and `B`.

6. Compute the posterior means for `A` and `B`.  How much should their ratings change based on this outcome?

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here

## Summary

In this notebook I started with the "outer" operations, like outer product and outer sum; then we used them to construct a joint distribution.

In general, you cannot construct a joint distrubution from two marginal distributions, but in the special case where the distributions are independent, you can.

We extended the Bayesian update process we've seen in previous notebook and applied it to a joint distribution.  Then from the posterior joint distribution we extracted posterior marginal distributions and posterior conditional distributions.

As an exercise, you had a chance to apply the same process to a slightly more difficult problem, updating Elo ratings based on the outcome of a chess game.

[In the next notebook](https://colab.research.google.com/github/AllenDowney/BiteSizeBayes/blob/master/12_sum.ipynb) we'll get back to a problem we left half-finished: the Euro problem.


import scipy as sp
from timer import timer

def entropy(values):
    """A slow way to calculate the entropy of the input values"""

    values = sp.asarray(values).flatten()
    #calculate the probablility of a value in a vector
    vUni = sp.unique(values)
    lenval = float(values.size)

    FreqData = sp.zeros(vUni.shape, dtype=float)
    for i in xrange(FreqData.size):
        FreqData[i] = sum(values==vUni[i])/lenval

    return -sum([FreqData[i]*sp.math.log(FreqData[i],2) for i in xrange(FreqData.size)])

def entropy2(values):
    """Calculate the entropy of vector values.

    values will be flattened to a 1d ndarray."""

    values = sp.asarray(values).flatten()
    p = sp.diff(sp.c_[0,sp.diff(sp.sort(values)).nonzero(), values.size])/float(values.size)
    H = (p*sp.log2(p)).sum()
    return -H

def chebyshev2(values, degree=1):
    """Calculate the Chebyshev Polynomials using previous results"""

    values = sp.asarray(values)
    A = sp.zeros((degree, len(values)))

    A[0,:]=1
    try:
        A[1,:]=values
    except IndexError:
        return A

    for i in range(2,degree):
        for x in range(len(values)):
            A[i,x] = 2*values[x]*A[i-1,x]-A[i-2,x]

    return A

def chebyshev2_lc(values, degree=1):
    """Calculate the Chebyshev Polynomials using previous results"""

    values = sp.asarray(values)
    A = sp.zeros((degree, len(values)))

    A[0,:]=1
    try:
        A[1,:]=values
    except IndexError:
        return A

    for i in range(2,degree):
        A[i,:] = [2*x for x in values]*A[i-1,:]-A[i-2,:]

    return A

def chebyshev_sp(values, degree=1):
    """Calculate the Chebyshev Polynomials using the scipy functions"""

    values = sp.asarray(values)
    A = sp.zeros((degree, len(values)))

    A[0,:]=1
    try:
        A[1,:]=values
    except IndexError:
        return A

    for i in range(2,degree):
        A[i,:] = sp.cos(i*sp.arccos(values))

    return A

def chebyshev_vec(values, degree=1):
    """Calculate the Chebyshev Polynobials

    This implementation uses sp.vectorize to vectorize python's math functions)"""

    values = sp.asarray(values)
    A = sp.zeros((degree, len(values)))

    A[0,:]=1
    try:
        A[1,:]=values
    except IndexError:
        return A

    cos = sp.vectorize(sp.math.cos)
    acos = sp.vectorize(sp.math.acos)

    for i in range(2,degree):
        A[i,:] = cos(i*acos(values))

    return A

def chebyshev_lc(values, degree=1):
    """Calculate the Chebyshev Polynomials using list comprehensions"""

    values = sp.asarray(values)
    A = sp.zeros((degree, len(values)))

    A[0,:]=1
    try:
        A[1,:]=values
    except IndexError:
        return A

    for i in range(2,degree):
        A[i,:] = [sp.math.cos(y) for y in [i*sp.math.acos(x) for x in values]]

    return A

def chebyshev(values, degree=1):
    """Calculate the Chebyshev Polynomial using

    Tn(x) = cos(n*cosh(x))"""
    values = sp.asarray(values)
    A = sp.zeros((degree, len(values)))

    A[0,:]=1
    try:
        A[1,:]=values
    except IndexError:
        return A

    for i in range(2,degree):
        for x in values:
            A[i,:] = sp.math.cos(i*sp.math.acos(x))
    return A

if __name__ == '__main__':
    from timer import timer
    testvals = sp.linspace(-1,1,500)
    funcs = [chebyshev, chebyshev_lc, chebyshev_vec, chebyshev_sp, chebyshev2, chebyshev2_lc]
    with timer(loops=5) as t:
        for f in funcs:
            t.time(f, testvals, 100)
        t.printTimes()


#
#  file:  gd_2d.py
#
#  2D example of gradient descent
#
#  RTK, 14-Feb-2021
#  Last update:  14-Feb-2021
#
################################################################

import numpy as np
import matplotlib.pylab as plt

#  Function and partial derivatives
def f(x,y):
    return 6*x**2 + 9*y**2 - 12*x - 14*y + 3

def dx(x):
    return 12*x - 12

def dy(y):
    return 18*y - 14

#  Gradient descent steps
N = 100
x,y = np.meshgrid(np.linspace(-1,3,N), np.linspace(-1,3,N))
z = f(x,y)
plt.contourf(x,y,z,10, cmap="Greys")
plt.contour(x,y,z,10, colors='k', linewidths=1)
plt.plot([0,0],[-1,3],color='k',linewidth=1)
plt.plot([-1,3],[0,0],color='k',linewidth=1)
plt.plot(1,0.7777778,color='k',marker='+')

x = xold = -0.5
y = yold = 2.9
for i in range(12):
    plt.plot([xold,x],[yold,y], marker='o', linestyle='dotted', color='k')
    xold = x
    yold = y
    x = x - 0.02 * dx(x)
    y = y - 0.02 * dy(y)

x = xold = 1.5
y = yold = -0.8
for i in range(12):
    plt.plot([xold,x],[yold,y], marker='s', linestyle='dotted', color='k')
    xold = x
    yold = y
    x = x - 0.02 * dx(x)
    y = y - 0.02 * dy(y)

x = xold = 2.7
y = yold = 2.3
for i in range(12):
    plt.plot([xold,x],[yold,y], marker='<', linestyle='dotted', color='k')
    xold = x
    yold = y
    x = x - 0.02 * dx(x)
    y = y - 0.02 * dy(y)

plt.xlabel("$x$")
plt.ylabel("$y$")
plt.tight_layout(pad=0, w_pad=0, h_pad=0)
plt.savefig("gd_2d_steps.png", dpi=300)
plt.show()
plt.close()

#  New function and partial derivatives
def f(x,y):
    return 6*x**2 + 40*y**2 - 12*x - 30*y + 3

def dx(x):
    return 12*x - 12

def dy(y):
    return 80*y - 30

#  Large stepsize
N = 100
x,y = np.meshgrid(np.linspace(-1,3,N), np.linspace(-1,3,N))
z = f(x,y)
plt.contourf(x,y,z,10, cmap="Greys")
plt.contour(x,y,z,10, colors='k', linewidths=1)
plt.plot([0,0],[-1,3],color='k',linewidth=1)
plt.plot([-1,3],[0,0],color='k',linewidth=1)
plt.plot(1,0.375,color='k',marker='+')

x = xold = -0.5
y = yold = 2.3
for i in range(14):
    plt.plot([xold,x],[yold,y], marker='o', linestyle='dotted', color='k')
    xold = x
    yold = y
    x = x - 0.02 * dx(x)
    y = y - 0.02 * dy(y)

x = xold = 2.3
y = yold = 2.3
for i in range(14):
    plt.plot([xold,x],[yold,y], marker='s', linestyle='dotted', color='k')
    xold = x
    yold = y
    x = x - 0.01 * dx(x)
    y = y - 0.01 * dy(y)

plt.xlabel("$x$")
plt.ylabel("$y$")
plt.tight_layout(pad=0, w_pad=0, h_pad=0)
plt.savefig("gd_2d_oscillating.png", dpi=300)
plt.show()
plt.close()


import numpy as np
import math
from sklearn.neighbors import kneighbors_graph
from sklearn.linear_model import LassoLars, Lars
from scipy.linalg import eigh
from fsfc.base import KBestFeatureSelector

class MCFS(KBestFeatureSelector):
    """
    Multi-Class Feature selection algorithm.

    Uses k-NN graph of samples in dataset and Spectral Graph Theory to find the most explaining features.

    Based on the article `"Unsupervised feature selection for multi-cluster data." <https://dl.acm.org/citation.cfm?id=1835848>`_.

    Algorithm selects features in the following way:

        1. Computes k-NN graph for the dataset.
        2. Computes heat matrix for this graph, degree and laplacian matrix.
        3. Solves eigen-problem: L y = `lambda` D y, selects K smallest eigen-values and corresponding vectors.
        4. Solves K regression problems, trying to predict every eigenvector by regression using dataset.
        5. Computes score of each feature using found regression coefficients.
        6. Select k features with the top scores.

    Parameters
    ----------
    k: int
        Number of features to select.
    clusters: int
        Expected number of datasets.
    p: int (default 8)
        Number of nearest neighbours for construction of k-NN graph.
    sigma: int (default 1)
        Coefficient for computation of heat matrix.
    mode: 'default' or 'lasso' (default 'default')
        Type of penalty for the method: with 'default' algorithm uses no penalty, with 'lasso' it uses L1-penalty.
    alpha: float (default 0.01)
        Importance of penalty for algorithm with **mode='lasso'**.
    """

    def __init__(self, k, clusters, p=8, sigma=1, mode='default', alpha=0.01):
        super().__init__(k)
        self.clusters = clusters
        self.p = p
        self.mode = mode
        self.sigma = sigma
        self.alpha = alpha

    def _create_regressor(self):
        if self.mode == 'default':
            return Lars()
        if self.mode == 'lasso':
            return LassoLars(alpha=self.alpha)
        raise ValueError('Unexpected mode ' + self.mode + '. Expected "default" or "lasso"')

    def _calc_scores(self, x):
        graph = kneighbors_graph(
            x,
            n_neighbors=self.p,
        )
        # Construct the heat matrix
        w = np.zeros([x.shape[0], x.shape[0]])
        rows, cols = graph.nonzero()
        for i, j in zip(rows, cols):
            w[i, j] = math.exp(-np.linalg.norm(x[i] - x[j])**2/self.sigma)

        # Compute degree and Laplacian matrices
        degree_vector = np.sum(w, 1)
        degree = np.diag(degree_vector)
        laplacian = degree - w

        # Solve the eigen-problem
        values, vectors = eigh(laplacian, degree)
        smallest = vectors[:, 0:self.clusters].T

        # Find coefficients for each cluster
        coefs = []
        for i in range(self.clusters):
            this_coefs = self._create_regressor().fit(x, smallest[i]).coef_
            coefs.append(this_coefs)
        coefs = np.array(coefs)

        # Compute MCFS-scores
        scores = np.max(coefs, 0)
        return scores


from numpy import *
import matplotlib.pyplot as plt
#http://systems-sciences.uni-graz.at/etextbook/sw2/lyapunov.html

result = []
lambdas = []
lambda_max=[]
maps = []
xmin = 2.9
xmax = 3.1
mult = int((xmax - xmin)*2000)

rvalues = arange(xmin, xmax, 0.001)

for r in rvalues:
    x = 0.1
    result = []
    for t in range(100):
        x = r * x * (1 - x)
        result.append(log(abs(r - 2*r*x)))
    lambdas.append(mean(result))
    lambda_max.append(max(result))
    # ignore first 100 iterations as transient time
    # then iterate anew
    for t in range(20):
        x = r * x * (1 - x)
        maps.append(x)

fig = plt.figure(figsize=(10,7))
ax1 = fig.add_subplot(1,1,1)

xticks = linspace(xmin, xmax, mult)

# zero line
zero = [0]*mult
ax1.plot(xticks, zero, 'g-')
ax1.plot(xticks, maps, 'r.',alpha = 0.3, label = 'Logistic map')
ax1.set_xlabel('r')
ax1.plot(rvalues, lambdas, 'b-', linewidth = 3, label = 'Lyapunov exponent mean')
ax1.plot(rvalues, lambda_max, 'p-', linewidth = 3, label = 'Lyapunov exponent max')
ax1.grid('on')
ax1.set_ylim(-1, 1)
ax1.set_xlabel('r')
ax1.legend(loc='best')
ax1.set_title('Logistic map versus Lyapunov exponent')

plt.show()


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

# In[1]:

import pandas as pd
import numpy as np
import math
import random
import matplotlib.pyplot as plt
plt.style.use("seaborn-darkgrid")
import os

# In[2]:

def coordinates_on_circle(n):
    """Returns x,y coordinates of points on an unit circle with spacing 2π/n"""
    if type(n)!=int:
        raise Exception("Wrong input: \n the argument must be an integer number of points on the unit circle!")
    x,y = [],[]
    step_angle = 2*math.pi/n
    for i in range(0,n):
        x.insert(len(x),math.cos(i*step_angle))
        y.insert(len(y),math.sin(i*step_angle))
    return x,y

# In[3]:

def create_starting_graph(n,r):
    if type(r)!=int:
        raise Exception("Wrong input: \n r must be an integer number of edges between vertices")
    if r>n-1:
        raise Exception("Wrong input: \n r must not exceed n-1!")
    coords = coordinates_on_circle(n)

    #create adjacency_matrix as pandas df
    #Initialize adjacency matrix
    adj_mat_df = pd.DataFrame(np.zeros([n,n]),dtype='bool')
    #Make starting connections with pbc
    for i in range(0,n):
        #left
        if(i-r>=0):
            adj_mat_df.iloc[i][i-r:i] = True
        else:
            diff = r-i
            adj_mat_df.iloc[i][0:i] = True
            adj_mat_df.iloc[i][n-diff:n+1] = True
        #right
        if(i+r<n):
            adj_mat_df.iloc[i][i+1:i+r+1] = True #+1 to avoid self loop and up to sym value
        else:
            diff = i+r-n
            adj_mat_df.iloc[i][i+1:n+1] = True
            adj_mat_df.iloc[i][0:diff+1] = True

    return adj_mat_df

# In[4]:

def create_strogatz(n, r ,p, place_labels=False ):
    """Plots the graph of the Strogatz model on a unit circle."""

    #Procedure to create results folder automatically
    path = os.getcwd()
    results_dir = "/results_WS"
    try:
        os.mkdir(path+results_dir)
    except OSError:
        print ("Creation of the directory %s failed" % results_dir)
    else:
        print ("Successfully created the directory %s " % results_dir)
    #names for file paths
    name_plot = "/plot_n%d_r%d_p%.3f.png" %(n,r,p)
    name_csv = "/data_n%d_r%d_p%.3f.csv" %(n,r,p)
    name_plot_rewired = "/plot_rewired_n%d_r%d_p%.3f.png" %(n,r,p)
    name_csv_rewired = "/data_rewired_n%d_r%d_p%.3f.csv" %(n,r,p)

    #check for errors
    if p>1 or p<0:
        raise Exception("Wrong input: \n p must be in [0,1]")
    coords = coordinates_on_circle(n)
    adj_mat = create_starting_graph(n,r)
    labels_nodes = []
    nodes_coords = coordinates_on_circle(n)
    #figure settings
    fig, (ax1, ax2) = plt.subplots(1, 2,figsize=(16,9))
    plt.subplots_adjust(wspace=0.3)
    plt.suptitle("WS(N=%d; 2r = %d), Starting configuration"%(n,2*r),fontsize=25)
    #plot graph
    for i in range(0,n):
        connections_list = adj_mat[adj_mat.iloc[i] == True].index.tolist()
    #print(connections_list)
        for k in range(0,len(connections_list)):
            ax1.plot([nodes_coords[0][i],nodes_coords[0][connections_list[k]]],[nodes_coords[1][i],nodes_coords[1][connections_list[k]]],linewidth=.5,color='indianred')
    ax1.plot(nodes_coords[0],nodes_coords[1],color='steelblue',ls='none',marker='o',markersize=10,label=labels_nodes)
    ax1.set_title("Graph representation",fontsize=20)
    ax1.set_xticks([])
    ax1.set_yticks([])
    #labels on vertices
    if place_labels==True:
        for i in range(0,n):
            labels_nodes.append("%d"%i)
            ax1.text(nodes_coords[0][i],nodes_coords[1][i],labels_nodes[i],fontsize=15)

    #plot adjacency matrix
    ax2.set_xlabel("Edges",fontsize=20)
    ax2.set_ylabel("Vertices",fontsize=20)
    ax2.matshow(adj_mat,cmap='cividis')
    ax2.set_title("Adjacency matrix",fontsize=25)

    #save things!
    adj_mat.to_csv(path+results_dir+name_csv,header=False, index=False)
    plt.savefig(path+results_dir+name_plot,dpi=200)
    plt.show()

    #print("PRE REWIRING:",sum(adj_mat))

    #rewiring! (anticlockwise, for sake of indices)

    for i in range(0,n):
        #print("working on row # %d"%(i))
        #edge_list = list(adj_mat[adj_mat.iloc[i] == True].index.tolist())
        #edge_list = [k for k in edge_list if k > i]
        for j in range(0,r):  #for each link to vertex i

            if (random.random()<p): #attempt a rewire
                #performing the rewire
                #    - Choose which of the connected edge to rewire->deleated_edge
                #    - Choose were to rewire it among the available positions->candidates
                #    - Perform the connection/deleate old connection/update mirror adjmat

                #choose which edge to remove: [+periodic boundary conditions]
                deleated_edge = i+1+j
                if deleated_edge>n-1:
                    deleated_edge = deleated_edge-n

                #chose available position:
                candidates = list(adj_mat[adj_mat.iloc[i] == False].index.tolist())
                candidates.remove(i) #take out self loop
                new_edge = random.choice(candidates)

                #print("candidates list = ",candidates)
                #print("new edge chosen = ",new_edge)

                #create new wire
                adj_mat.iloc[i][new_edge]=True
                adj_mat.iloc[new_edge][i]=True

                #deleate old wire
                adj_mat.iloc[i][deleated_edge]=False
                adj_mat.iloc[deleated_edge][i]=False

    #print("AFTER REWIRING:",sum(adj_mat))

    #Plot rewired

    fig, (ax1, ax2) = plt.subplots(1, 2,figsize=(16,9))
    plt.subplots_adjust(wspace=0.3)
    plt.suptitle("WS(N=%d; 2r = %d; p = %.3f)"%(n,2*r, p),fontsize=25)

    #plot graph
    for i in range(0,n):
        connections_list = adj_mat[adj_mat.iloc[i] == True].index.tolist()
    #print(connections_list)
        for k in range(0,len(connections_list)):
            ax1.plot([nodes_coords[0][i],nodes_coords[0][connections_list[k]]],[nodes_coords[1][i],nodes_coords[1][connections_list[k]]],linewidth=.5,color='indianred')
    ax1.plot(nodes_coords[0],nodes_coords[1],color='steelblue',ls='none',marker='o',markersize=10,label=labels_nodes)
    ax1.set_title("Graph representation",fontsize=20)
    ax1.set_xticks([])
    ax1.set_yticks([])
    #labels on vertices
    if place_labels==True:
        for i in range(0,n):
            labels_nodes.append("%d"%i)
            ax1.text(nodes_coords[0][i],nodes_coords[1][i],labels_nodes[i],fontsize=15)

    #plot adjacency matrix
    ax2.set_xlabel("Edges",fontsize=20)
    ax2.set_ylabel("Vertices",fontsize=20)
    ax2.matshow(adj_mat,cmap='cividis')
    ax2.set_title("Adjacency matrix",fontsize=25)

    #save things!
    adj_mat.to_csv(path+results_dir+name_csv_rewired,header=False, index=False)
    plt.savefig(path+results_dir+name_plot_rewired,dpi=200)
    plt.show()
    return adj_mat

# In[82]:

n,r,p=50,5,.3
adj_mat = create_strogatz(n,r,p)


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

# Import pagerank_adj.mat matrix (2571 x 2571)
mat = scipy.io.loadmat('pagerank_adj.mat')["J"]

x = np.ones((2571, 1))

# Sum of all columns in mat
column_sums = np.zeros(2571)
for col in range(2571):
    for row in range(2571):
        column_sums[col] += mat[row][col]

# Create matrix A
A = np.zeros((2571, 2571))
for row in range(2571):
    for col in range(2571):
        A[row][col] = mat[row][col] / column_sums[col]

# Verify columns of A sum to 1 using matrix multiplication
result = A.transpose().dot(x)  # Results in vector of ones

# Normalize x
x_norm = x / np.linalg.norm(x)

# Vector of log of error terms
log_error_PI = np.zeros(10)
log_error_SIPI = np.zeros(10)
log_error_rayleigh = np.zeros(10)

# Power iteration algorithm
for k in range(10):
    y = A.dot(x_norm)
    x_norm = y / np.linalg.norm(y)
    evalue = x_norm.transpose().dot(A.dot(x_norm))
    log_error_PI[k] = np.log10(np.linalg.norm(A.dot(x_norm) - x_norm))

# Shift-invert power iteration algorithm
x_norm = x / np.linalg.norm(x)
sigma = 0.99
for k in range(10):
    y = np.linalg.inv(A - sigma * np.identity(2571)).dot(x_norm)
    x_norm = y / np.linalg.norm(y)
    evalue = x_norm.transpose().dot(A.dot(x_norm))
    log_error_SIPI[k] = np.log10(np.linalg.norm(A.dot(x_norm) - x_norm))

# Rayleigh quotient iteration algorithm
x_norm = x / np.linalg.norm(x)
for k in range(10):
    # Index starts at 0
    if k > 1:
        sigma = (x_norm.transpose().dot(A).dot(x_norm)) / (x_norm.transpose().dot(x_norm))
    y = np.linalg.inv(A - sigma * np.identity(2571)).dot(x_norm)
    x_norm = y / np.linalg.norm(y)
    log_error_rayleigh[k] = np.log10(np.linalg.norm(A.dot(x_norm) - x_norm))

# Plot log of the error
plt.plot(range(10), log_error_PI, label="Power iteration error")
plt.plot(range(10), log_error_SIPI, label="Shift-invert power iteration error")
plt.plot(range(10), log_error_rayleigh, label="Rayleigh quotient iteration error")
plt.xlabel('k value')
plt.ylabel('log(e(k+1))')
plt.title('Log error value plot')
plt.legend()

# PageRank score is the approximated eigenvector x(k+1)
top_five_indices = sorted(range(len(x_norm)), key=lambda i: x_norm[i])[-5:]
bottom_five_indices = sorted(range(len(x_norm)), key=lambda i: x_norm[i])[:5]

for p in top_five_indices: print(p)
for p in bottom_five_indices: print(p)

plt.show()


from __future__ import annotations
from typing import Union
import numpy as np
import random

class KMeans:
    """KMeans
    Parameters:
    -----------
    n_clusters: int = 2
        Number of clusters
    tol: float = 0.0001
        Relative tolerance
    max_iter: int = 300
        Maximum number of iterations of the k-means algorithm for a single run
    """
    def __init__(self, n_clusters: int = 2, tol: float = 0.0001, max_iter: int = 300) -> None:
        self.n_clusters = n_clusters
        self.tol = tol
        self.max_iter = max_iter
        self.centroids = {}
        self.classifications = {}

    def fit(self, data: Union[list, np.ndarray]) -> KMeans:
        self.centroids = {}

        # Randomly initialize centroids
        for i in range(self.n_clusters):
            self.centroids[i] = data[random.randint(0, len(data)-1)]

        for _ in range(self.max_iter):
            self.classifications = {}

            for i in range(self.n_clusters):
                self.classifications[i] = []

            # put row into the right cluster
            for row in data:
                distances = [np.linalg.norm(
                    row-self.centroids[centroid]) for centroid in self.centroids]
                classification = distances.index(min(distances))
                self.classifications[classification].append(row)

            prev_centroids = dict(self.centroids)

            for classification in self.classifications:
                self.centroids[classification] = np.average(
                    self.classifications[classification], axis=0)

            # Check if we are finished
            optimized = True
            for c in self.centroids:
                original_centroid = prev_centroids[c]
                current_centroid = self.centroids[c]
                if np.sum((current_centroid - original_centroid) / original_centroid * 100.0) > self.tol:
                    optimized = False

            if optimized:
                break
        return self

    def predict(self, data: Union[list, np.ndarray]) -> list:
        classifications = []
        for row in data:
            distances = [np.linalg.norm(row-self.centroids[centroid])
                         for centroid in self.centroids]
            classification = distances.index(min(distances))
            classifications.append(classification)
        return classifications

if __name__ == '__main__':
    import matplotlib.pyplot as plt
    from matplotlib import style
    from sklearn.datasets import make_blobs
    style.use('ggplot')

    X, y = make_blobs(n_samples=30, centers=3, n_features=2)

    model = KMeans(n_clusters=3)
    model.fit(X)

    centroids = model.centroids

    colors = 10 * ['r', 'g', 'b', 'c', 'k', 'y']

    for classification, featureset in zip(model.predict(X), X):
        color = colors[classification]
        plt.scatter(featureset[0], featureset[1], marker="x",
                    color=color, s=150, linewidths=5, zorder=10)

    for c in centroids:
        plt.scatter(centroids[c][0], centroids[c][1],
                    color='k', marker="*", s=150, linewidths=5)

    plt.show()


import numpy as np
import sys

print(f'Python Version is: {sys.version}')

# Define a Scalar
x = 6

# Define a Vector
z = np.array((1, 2, 3, 4))

print(f'Vector Dimension {z.shape}')
print(f'Vector size {z.size}')

# Define a matrix

m = np.array(([1, 2, 3],
              [4, 5, 6],
              [7, 8, 9]))

print(f'Matrix Dimension {m.shape}')
print(f'Matrix size {m.size}')

one = np.zeros((3, 4), dtype=np.float)
"""
Output:

[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]
"""

# Define a Tensor

tensor = np.ones((2, 2, 2), dtype=np.float)
"""
Output:
[[[1. 1.]
  [1. 1.]]

 [[1. 1.]
  [1. 1.]]]
"""

# Indexing

tensor[0, 1, 1] = 3.4
print(tensor)
"""
Output:
[[[1.  1. ]
  [1.  3.4]]

 [[1.  1. ]
  [1.  1. ]]]
"""

# Matrix Transpose

array = np.arange(6).reshape((2, 3))
"""
Output:
[[0 1 2]
 [3 4 5]]
"""
array = array.transpose()
"""
Output:
[[0 3]
 [1 4]
 [2 5]]
"""


#!/usr/bin/env python3
"""
Operations for laplacian decomposition.

Attributes
----------
LGR
    Logger
"""

import logging

import numpy as np

LGR = logging.getLogger(__name__)

def symmetric_normalisation(mtx):
    """
    Compute symmetrically normalised Laplacian matrix.

    Parameters
    ----------
    mtx : numpy.ndarray
        A [structural] matrix

    Returns
    -------
    numpy.ndarray
        The symmetrically normalised version of mtx

    See Also
    --------
    https://en.wikipedia.org/wiki/Laplacian_matrix#Symmetrically_normalized_Laplacian_2
    """
    d = np.diag(mtx.sum(axis=-1) ** (-1/2))

    symm_norm = (d @ mtx @ d)

    return (np.eye(mtx.shape[0]) - symm_norm)

def decomposition(mtx):
    """
    Run a eigenvector decomposition on input.

    Parameters
    ----------
    mtx : numpy.ndarray
        The matrix to decompose

    Returns
    -------
    numpy.ndarray
        The eigenvalues resulting from the decomposition
    numpy.ndarray
        The eigenvectors resulting from the decomposition
    """
    eigenval, eigenvec = np.linalg.eig(mtx)

    idx = np.argsort(eigenval)
    eigenval = eigenval[idx]
    # #!# Check that eigenvec has the right index and not inverted
    eigenvec = eigenvec[:, idx]

    return eigenval, eigenvec

"""
Copyright 2022, Stefano Moia.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""


from numpy import zeros

# Define euler_set function
def euler_set(f,t0,tf,y0,v0,n):
    h = (tf - t0)/n
    t = zeros(n+1)
    y = zeros(n+1)
    v = zeros(n+1)
    t[0] = t0
    y[0] = y0
    v[0] = v0
    for i in range(n):
        y[i+1] = y[i] + h*v[i]
        v[i+1] = v[i] + h*f(t[i],y[i],v[i])
        t[i+1] = t[i] + h
    return t,y,v

# Define functions
def f(t,y,v):
    return (t**2 - 1.0)*y

# Set initial conditions
t0 = 0
tf = 1
y0 = 1.0
v0 = 0.0
n = 10

# Execute EULER_SET
t, y, v = euler_set(f,t0,tf,y0,v0,n)

# Print results
print('%5s %8s %8s' % ('t','y','v'))
for i in range(n+1):
    print("%9.4f %9.4f %9.4f" % (t[i],y[i],v[i]))


# -*- coding: utf-8 -*-
import sympy as sp

p = sp.symbols('p')

print("log'(p) = {0}".format(sp.diff(sp.log(p)).simplify()))
print("(p*log(p))' = {0}".format(sp.diff(p * sp.log(p)).simplify()))
print("((1-p)*log(1-p))' = {0}".format(sp.diff((1-p) * sp.log(1 - p)).simplify()))

H = -(p * sp.log(p) + (1 - p) * sp.log(1 - p))

print("H'(p) = {0}".format(sp.diff(H, p).simplify()))


import numpy as np
from scipy import stats
import math

x = np.array([5,15,25,35,45])
x_square = np.square(x)

f = np.array([6,16,24,25,19])
cum_freq = np.array([6,22,46,71,90])
f_x_square = f*x_square

# mean
mean = np.average(x,weights=f)

# mode for now find manually
l = 30
f0 = 10
f1 = 12
f2 = 8
c = 10
mode  = l + ((f1 - f0)/(2*f1 - c*f0 + f2))*c
mode  = 31.4

# standard deviation
sum_f_x_square = np.sum(f_x_square)
sum_f = np.sum(f)
mean_square = math.pow(mean,2)
standard_deviation = math.sqrt((sum_f_x_square/sum_f) - mean_square)

print("mean : ",mean)
print("mode : ",mode)
print("standard_deviation : ",standard_deviation)

coeff = (mean - mode)/standard_deviation

print("Coefficent : ",coeff)


# Kalman Filtering Example
#
# Noisy Sensor
#
# Author: Adam Noel
#

import random
import numpy as np
import matplotlib.mlab as mlab
import matplotlib.pyplot as plt

class NoisySensor:
	def __init__(self, sensorValue, noise):
		self.sensorValue = sensorValue
		self.noise = noise
	def getValue(self):
		return (self.sensorValue + np.random.normal(0, 0.5))

class Averager:
	def __init__(self, initialValue):
		self.currentValue = initialValue
		self.numValues = 1
	def updateEstimate(self, newObservation):
		self.numValues = self.numValues + 1
		self.currentValue = (1/float(self.numValues))*newObservation + ((self.numValues - 1)/float(self.numValues))*self.currentValue
	def getValue(self):
		return self.currentValue

class KalmanFilter:
	def __init__(self, shi, Q, H, R, x, P):
		self.shi = shi
		self.Q = Q
		self.H = H
		self.R = R
		self.x = x
		self.P = P
	def updateState(self, newObservation):
		#Now, project to current state
		self.x = self.shi.dot(self.x)
		self.P = self.shi.dot(self.P).dot(np.transpose(self.shi)) + self.Q

		#First, compute the Kalman Gain
		innerKalmanProduct = np.linalg.inv(H.dot(P).dot(np.transpose(H)) + R)
		kalmanGain = self.P.dot(np.transpose(H)).dot(innerKalmanProduct)

		#Next, update the estimate
		self.x = self.x + kalmanGain.dot(np.array(newObservation) - H.dot(self.x))

		#Next, update the error covariance matrix
		sizeP = self.P.shape[0] #Get the shape of the error matrix
		self.P = (np.eye(sizeP) - kalmanGain.dot(H)).dot(self.P)

	def getState(self):
		return np.asarray(self.x)[0][0] ##Convert from matrix to array

numSteps = 100
sensorNoise = 0.5
sensorValue = 3
firstStateEstimate = 2.6
firstErrorEstimate = 0.5

actualValues = []
observedValues = []
averagedValues = [] #Averager to serve as a kalman filter reference
kalmanValues = []

shi = np.matrix([1])
Q = np.matrix([0.00001])
H = np.matrix([1])
R = np.matrix([sensorNoise])
x = np.matrix([firstStateEstimate])
P = np.matrix([firstErrorEstimate])

kalmanFilter = KalmanFilter(shi, Q, H, R, x, P)
noisySensor = NoisySensor(sensorValue, sensorNoise)
averager = Averager(firstStateEstimate)

for i in range(0, numSteps):
	newObservation = noisySensor.getValue()
	actualValues.append(sensorValue)
	observedValues.append(newObservation)
	averager.updateEstimate(newObservation)
	averagedValues.append(averager.getValue())
	kalmanFilter.updateState(newObservation)
	kalmanValues.append(kalmanFilter.getState())

steps = range(0, numSteps)
plt.plot(steps, actualValues, 'r--', label='Actual')
plt.plot(steps, observedValues, 'bs', label='Observed')
plt.plot(steps, averagedValues, 'g^', label='Averaged')
plt.plot(steps, kalmanValues, 'ko', label='Kalman')
plt.legend(loc='lower right')
#plt.plot(steps, actualValues, 'r--', label='Actual', steps, observedValues, 'bs', label='Observed', steps, averagedValues, 'g^', label='Averaged', steps, kalmanValues, 'ko', label='Kalman')
plt.show()


import numpy as np
import matplotlib.pyplot as plt

def plot_complex_exponential(A, f, phi, fs):

    Ts    = 1/fs
    Tr    = 1/(f*100)
    ts    = np.arange(0, 1+Ts, Ts)
    tr    = np.arange(0, 1+Tr, Tr)
    cexps = np.exp(1j*(2*np.pi*f*ts+phi))
    cexpr = np.exp(1j*(2*np.pi*f*tr+phi))

    real_cexpr = np.real(cexpr)
    imag_cexpr = np.imag(cexpr)

    real_cexps = np.real(cexps)
    imag_cexps = np.imag(cexps)

    ys     = [(real_cexps, real_cexpr), (imag_cexps, imag_cexpr)]
    titles = ["Parte real", "Parte imaginária"]
    fig, axs = plt.subplots(2,1,figsize = (20,6))

    for y, title, ax in zip(ys, titles, axs.flatten()):
        y_sample, y_real = y
        ax.scatter(ts, y_sample, label = "Amostras", c = 'k')
        ax.plot(tr, y_real, label = '"Dados reais"', c='r')
        ax.set_xlabel("Tempo (s)", fontsize = 14)
        ax.set_ylabel("Amplitude",fontsize = 14)
        ax.set_title(title,fontsize = 22)
        ax.legend(fontsize=16)

    plt.tight_layout()
    plt.show()

def plot_DFT(input_signal, plot=True):

    X_k = []

    N  = len(input_signal)
    ns = np.arange(-N/2, N/2)
    ks = np.arange(-N/2, N/2)

    for k in ks:
        s_k = np.exp(-1j*(2*np.pi*k)/N * ns)
        X_k.append(sum(input_signal*s_k))

    if plot:
        fig, axs = plt.subplots(1,2,figsize = (20,6))

        axs[0].stem(ks, np.abs(X_k))
        axs[0].set_ylabel("Decibels",fontsize = 16)

        axs[1].stem(ks, np.unwrap(np.angle(X_k,deg=True),discont=180))
        axs[1].set_ylabel("Graus",fontsize = 16)

        for ax in axs.flatten():
            ax.set_xlabel("Frequência",fontsize = 16)
            ax.set_xlim(-N/2, N/2-1)

        plt.suptitle("Discrete Fourier Transform",fontsize = 22)
        plt.tight_layout()
        plt.show()

    return X_k


import numpy as np
rows = [[1, 2, 1],
        [3, 8, 1],
        [0, 4,  1]]

A = np.array(rows)
rank_A = np.linalg.matrix_rank(A)
assert rank_A == 3

## So the vectors are linear independent. And they span R3. The vectors form a basis in R3.

#Check the dimension
dim_A = A.shape

assert dim_A[0] == 3
assert dim_A[1] == 3

print(dim_A)


# Cálculo da razão áurea (phi)
import sympy

d = 20
phi = sympy.symbols('phi', nonnegative=True)

eqn = sympy.Eq(1/phi, phi - 1)

sol = sympy.solve(eqn)
sympy.pprint(sol)
phiAprox = sympy.N(sol[0], d)

print('Para ', d, ' dígitos significativos, ϕ = ', phiAprox)


#import multiple libraries
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import animation
import time

#CONSTANTS
#---------
iterations=1000 #quantity of "parts" that the flower is going to be divided by (the larger it becomes, the smoother it looks, the more time it takes)
fps = 100 #frames per second on the animation
make_animated = True #animation boolean
sleep_seconds = 10 #time the flower pauses after being plotted
color = 'red' #specify the color of the rose

#CODE
#----
first = True #boolean to check when to pause
fig = plt.figure() #initialize the figure
ax = plt.axes(xlim=(-1,1), ylim=(-1,1)) #set limits of the axes
rose, = ax.plot([],[],color) #initialize the rose, variable
user_input = input("Type in a number (in case of a fraction in its lowest terms [a/b]): ") #ask for k
coeffs = user_input.split("/") #split the user input by the fraction slash
num = float(coeffs[0]) #assign numerator value
if len(coeffs)>1: #check for a denominator
	den = float(coeffs[1]) #assign denominator value
else:
	den = 1 #if there's no denominator give a value of 1
k=num/den #assign the value to k
num_odd = False
den_odd = False
if num%2 != 0: #check if the numerator is odd
	num_odd = True #assign value True to boolean checking if the numerator is odd
if den%2 != 0: #check if the denominator is odd
	den_odd = True #assign value True to boolean checking if the denominator is odd

if num_odd and den_odd: #check if both numerator and denominator are odd
	angle = np.pi*den #assign the value of the angle to close
else:
	angle = 2*np.pi*den #assign the value of the angle to close
thetas = np.linspace(0, angle, iterations) #creation of all the values that are going to be computed
x = np.cos(k*thetas)*np.cos(thetas) #multiply by the cosinus to get all x coordinates
y = np.cos(k*thetas)*np.sin(thetas) #multiply by the sinus to get all y coordinates

def initialize(): #definition of initialize
	rose.set_data([],[]) #establish the array on the rose variable
	return rose,

def animate(i): #definition for the animation
	global first #modification of global variable final
	rose.set_data(x[0:i], y[0:i]) #add one more value of x and y to the rose array
	if i == 0 and not first: #check if the flower has been completed and it's not the first plotting
		time.sleep(sleep_seconds) #sleep for the specified time
	if first: #check if it is the very start of the plotting
		first=False; #know it's not the first time
	return rose,

if make_animated: #if animated
	animation.FuncAnimation(fig, animate, init_func=initialize, frames=iterations, interval=1000/fps, blit=True) #call the animation with all functions and user specifications
else:
	plt.plot(x,y, color) #plot the flower directly
plt.show() #show plots