import numpy as np

def booleanViewings(ratings):
    """
    Converts the information on which coefficients are non-zero into a boolean matrix
    :param ratings: Sparse matrix of the ratings, with non-zero coefficients when a rating is available
    :return: "Boolean" matrix with a 1 at position (f, u) if the rating (f, u) is available in the ratings matrix ; a 0
              otherwise
    """
    nF, nU = ratings.shape
    viewings = np.zeros((nF, nU))
    nnz_rows, nnz_cols = ratings.nonzero()
    for f, u in list(zip(nnz_rows, nnz_cols)):
        viewings[f, u] = 1
    return viewings

def sparseToDense(S):
    """
    Converts a scipy.sparse sparse matrix into a numpy dense array
    :param S: Input sparse matrix
    :return: Corresponding numpy array
    """
    return np.asarray(S.toarray())

def normalizeRatings(denseRatings, boolViewings):
    """
    Normalizes the ratings on a per-user basis
    :param denseRatings: The ratings matrix, given as a numpy array
    :param boolViewings: A boolean (0 or 1) matrix indicating the disponibility of the ratings (result of
           booleanViewings())
    :return: A 2D numpy array containing the normalized ratings, along with two 1D arrays, containing respectively the
             mean rating and the standard deviation of the ratings of each user
    The normalization process consists in replacing the rating r of a (f, u) film-user couple by (r - m)/s, where m, s
    are the mean/standard deviation of the ratings of u
    """
    user_means = np.sum(denseRatings, axis=0) / np.sum(boolViewings, axis=0)
    user_stdDevs = (np.sum(denseRatings * denseRatings, axis=0) / np.sum(boolViewings,
                                                                         axis=0)) - user_means * user_means
    normalized_ratings = boolViewings * (denseRatings - user_means) / user_stdDevs
    return normalized_ratings, user_means, user_stdDevs

def buildUsersGraph(normRatings, boolViewings, film_weights=[], verbose=True):
    """
    Builds a graph structure between the users, measuring the similarity of ratings of users who have seen at least one
    common film
    :param normRatings: The normalized ratings, result of normalizeRatings()
    :param boolViewings: A boolean (0 or 1) matrix indicating the disponibility of the ratings
                         (result of booleanViewings())
    :param film_weights: Optional weights, one for each film, applied to the terms concerning this film
                         in the similarity sum
    :param verbose: If set to True, status prints are made during the execution of the code
    :return: A similarity matrix between users, and a common viewings matrix indicating for users (u, v) the number of
             common films graded by both u and v
    The two output matrices describe the structure of a graph, whose vertices are the users, and an edge connects two
    users if their number of common viewings is > 0
    The similarity matrix provides weights for these edges, corresponding to a measure of how similar are the gradings
    of the connected users
    The similarity between two users is computed as the sum, over the films f they have both seen, of the product of
    their two grades of f, multiplied optionally by the weight of f
    This corresponds to a non-normalized correlation measure. Possible film weights are for instance the standard
    deviation of the gradings of each film, to give less weight to the films on which all users agree (this has not
    proved useful as of now)
    """
    nF, nU = normRatings.shape
    if len(film_weights) != nF:
        film_weights = np.ones(nF)
    similarities = np.zeros((nU, nU))
    user_commonViewings = np.zeros((nU, nU))
    for f in range(nF):
        if verbose and f % 100 == 0:
            print(f)
        users = np.where(boolViewings[f, :] == 1)[0]
        nRatings = len(users)
        for i in range(nRatings):
            for j in range(i + 1, nRatings):
                similarities[users[i], users[j]] += normRatings[f, users[i]] * normRatings[f, users[j]] * film_weights[
                    f]
                similarities[users[j], users[i]] = similarities[users[i], users[j]]
                user_commonViewings[users[i], users[j]] += 1
                user_commonViewings[users[j], users[i]] += 1
    return similarities, user_commonViewings

def buildFilmsGraph(normRatings, boolViewings, user_weights=[], verbose=True):
    """
    Builds a graph structure between the films, measuring the similarity of ratings of films who have been viewed by
    at least a common user
    :param normRatings: The normalized ratings, result of normalizeRatings()
    :param boolViewings: A boolean (0 or 1) matrix indicating the disponibility of the ratings
    (result of booleanViewings())
    :param user_weights: Optional weights, one for each user, applied to the terms concerning this user in the
                         similarity sum
    :param verbose: If set to True, status prints are made during the execution of the code
    :return: A similarity matrix between films, and a common viewers matrix indicating for films (f, g) the number of
    common users having graded both f and g
    The return arguments contain the structure of a graph, whose vertices are the films, and an edge connects two films
    if their number of common viewers is > 0
    The similarity matrix provides weights for these edges, corresponding to a measure of how similar are the gradings
    of the connected films
    The similarity between two films is computed as the sum, over the users u that have both seen them, of the product
    of his two grades, multiplied optionally by the weight of u
    This corresponds to a non-normalized correlation measure. Using user weights has not been tested as of now.
    """
    return buildUsersGraph(normRatings.transpose(), boolViewings.transpose(), user_weights, verbose)

def buildNeighbors(user_commonViewings):
    """
    Extracts the list of neighbors of each user from the commonViewings matrix
    :param user_commonViewings: Matrix indicating the number of common viewed films for each two users, as returned by
                                buildUsersGraph()
    :return: A list of lists, indicating for each user his neighbors in the graph, ie the users which have at least one
    film in common with him
    Note : can be used in the same way for the films point of view based model
    """
    nU = user_commonViewings.shape[0]
    neighbors = []
    for ui in range(nU):
        n = []
        for uj in range(nU):
            if user_commonViewings[ui, uj] > 0:
                n.append(uj)
        neighbors.append(n)
    return neighbors

def sortNeighbors(neighbors, similarities, useTuples=True, verbose=True):
    """
    Sorts each list of neighbors in decreasing order according to similarities between users
    :param neighbors: List of lists indicating the neighbors of each user, as returned by buildNeighbors()
    :param similarities: Similarity matrix between users, as returned by buildUsersGraph()
    :param useTuples: Boolean parameter, indicating if the function should return a list of list of tuples.
    :param verbose: If set to True, status prints are made during the execution of the code
    :return: A list of lists of tuples, with for each user its neighbors in decreasing order of similarity, in the form
    of (neighbor index, similarity) tuples.
    Only neighbors with positive similarity are keeped. If useTuples is set to False, the output is a simple list of
    lists, containing only neighbor indices instead of tuples to save memory
    Note : can be used in the same way for the films point of view based model
    """
    sorted_neighbors = []
    nU = similarities.shape[0]
    for ui in range(nU):
        if verbose and ui % 100 == 0:
            print(ui)
        n = [(uj, similarities[ui, uj]) for uj in neighbors[ui] if similarities[ui, uj] > 0]
        n = sorted(n, key=lambda x: x[1], reverse=True)
        if not useTuples:
            n1 = []
            for i, j in n:
                n1.append(i)
            sorted_neighbors.append(n1)  # To save memory
        else:
            sorted_neighbors.append(n)
    return sorted_neighbors

def buildUsersModel(ratings, verbose=True):
    """
    Does the whole model building process from the users point of view given the initial sparse ratings matrix
    :param ratings: The initial sparse ratings matrix
    :param verbose: If set to True, status prints are made during the execution of the code
    :return: The built model, as a dictionary containing the following entries:
        - 'nF' : The number of films (ie number of rows in the ratings matrix)
        - 'nU' : The number of users (ie number of columns in the ratings matrix)
        - 'boolViewings' : The boolean (0 or 1) matrix indicating which user has seen which film
        - 'denseRatings' : The ratings matrix in a dense form (numpy array)
        - 'normRatings' : A dense matrix containing the normalized (per user) ratings
        - 'userMeans' : The vector of mean grades given by users
        - 'userStdDevs' : The vector of standard deviations of the grades given by users
        - 'sortedNeighbors' : The list of lists of the neighbors of each user, along with similarities, in decreasing
                              order of similarity
    """
    nF, nU = ratings.shape
    boolViewings = booleanViewings(ratings)
    denseRatings = sparseToDense(ratings)
    normRatings, userMeans, userStdDevs = normalizeRatings(denseRatings, boolViewings)
    similarities, user_commonViewings = buildUsersGraph(normRatings, boolViewings, verbose=verbose)
    neighbors = buildNeighbors(user_commonViewings)
    sorted_neighbors = sortNeighbors(neighbors, similarities, useTuples=True, verbose=verbose)
    model = {'nF': nF, 'nU': nU, 'boolViewings': boolViewings, 'denseRatings': denseRatings, 'normRatings': normRatings,
             'userMeans': userMeans, 'userStdDevs': userStdDevs, 'sortedNeighbors': sorted_neighbors}
    return model

def buildFilmsModel(ratings, verbose=True):
    """
    Does the whole model building process from the films point of view given the initial sparse ratings matrix
    :param ratings: The initial sparse ratings matrix
    :param verbose: If set to True, status prints are made during the execution of the code
    :return: The built model, as a dictionary containing the following entries:
        - 'nF' : The number of films (ie number of rows in the ratings matrix)
        - 'nU' : The number of users (ie number of columns in the ratings matrix)
        - 'boolViewings' : The boolean (0 or 1) matrix indicating which user has seen which film
        - 'denseRatings' : The ratings matrix in a dense form (numpy array)
        - 'normRatings' : A dense matrix containing the normalized (per user) ratings
        - 'userMeans' : The vector of mean grades given by users
        - 'userStdDevs' : The vector of standard deviations of the grades given by users
        - 'similarities' : The matrix of similarities between films
        - 'sortedNeighbors' : The list of lists of the neighbors of each film, in decreasing order of similarity
    """
    nF, nU = ratings.shape
    if verbose:
        print('Preprocessing data')
    boolViewings = booleanViewings(ratings)
    denseRatings = sparseToDense(ratings)
    normRatings, userMeans, userStdDevs = normalizeRatings(denseRatings, boolViewings)
    if verbose:
        print('Building graph')
    similarities, film_commonViewings = buildFilmsGraph(normRatings, boolViewings, verbose=verbose)
    if verbose:
        print('Extracting neighbors')
    neighbors = buildNeighbors(film_commonViewings)
    if verbose:
        print('Sorting neighbors')
    sorted_neighbors = sortNeighbors(neighbors, similarities, useTuples=False, verbose=verbose)
    model = {'nF': nF, 'nU': nU, 'boolViewings': boolViewings, 'denseRatings': denseRatings, 'normRatings': normRatings,
             'userMeans': userMeans, 'userStdDevs': userStdDevs, 'similarities': similarities,
             'sortedNeighbors': sorted_neighbors}
    if verbose:
        print('Model built')
    return model

def usersPrediction(f, u, k, model, verbose=True):
    """
    Predicts the grade of a user for one film given the (users point of view based) model and the parameter k
    :param f: The film index
    :param u: The user index
    :param k: The parameter k of the k nearest neighbors algorithm
    :param model: The prediction model, as built by buildUsersModel()
    :param verbose: If set to True, status prints are made during the execution of the code
    :return: An estimation as a floating point number of the grade the user would give to the film
    The prediction is made by taking as many neighbors as possible of u, but at most k, who have seen the film f, chosen
    by decreasing order of similarity with u.
    The average of their normalized ratings, weighted by their similarities with u, is then computed as a prediction of
    the normalized grade of u for f.
    The grade is then multiplied by u's standard deviation, added to u's mean grade, and culled if necessary between 1
    and 5, to give the final estimate.
    If no neighbors of u with a positive similarity with u have seen the film f, the prediction is simply the average
    grade given by u.
    """
    count = 0
    ref_neighbors = []
    n_neighbors = len(model['sortedNeighbors'][u])
    while len(ref_neighbors) < k and count < n_neighbors:
        n, sim = model['sortedNeighbors'][u][count]
        if model['boolViewings'][f, n] == 1 and sim > 0:
            ref_neighbors.append((n, sim))
        count += 1
    if len(ref_neighbors) == 0:
        if verbose:
            print("No (correlated) neighbors have seen the film, returning average grade")
        return model['userMeans'][u]
    meanGrade = 0
    totWeights = 0
    for n, sim in ref_neighbors:
        meanGrade += (model['userMeans'][u] + model['userStdDevs'][u] * model['normRatings'][f, n]) * sim
        totWeights += sim
    meanGrade /= totWeights
    meanGrade = min(meanGrade, 5)
    meanGrade = max(meanGrade, 1)
    return meanGrade

def filmsPrediction(f, u, k, model, verbose=True):
    """
    Predicts the grade of a user for one film given the (films point of view based) model and the parameter k
    :param f: The film index
    :param u: The user index
    :param k: The parameter k of the k nearest neighbors algorithm
    :param model: The prediction model, as built by buildFilmsModel()
    :param verbose: If set to True, status prints are made during the execution of the code
    :return: An estimation as a floating point number of the grade the user would give to the film
    The prediction is made by taking as many neighbors as possible of f, but at most k, who have been graded by u,
    chosen by decreasing order of similarity with f.
    The average of their normalized grades by u, weighted by their similarities with f, is then computed as a prediction
    of the normalized grade of u for f.
    The grade is then multiplied by u's standard deviation, added to u's mean grade, and culled if necessary between 1
    and 5, to give the final estimate.
    If no neighbors of f with a positive similarity with f have been graded by u, the prediction is simply the average
    grade given by u.
    """
    count = 0
    ref_neighbors = []
    n_neighbors = len(model['sortedNeighbors'][f])
    while len(ref_neighbors) < k and count < n_neighbors:
        n = model['sortedNeighbors'][f][count]
        sim = model['similarities'][f, n]
        if model['boolViewings'][n, u] == 1 and sim > 0:
            ref_neighbors.append((n, sim))
        count += 1
    if len(ref_neighbors) == 0:
        if verbose:
            print("No (correlated) neighbors have been seen by the user")
        return model['userMeans'][u]
    meanGrade = 0
    totWeights = 0
    for n, sim in ref_neighbors:
        meanGrade += model['denseRatings'][n, u] * sim
        totWeights += sim
    meanGrade /= totWeights
    meanGrade = min(meanGrade, 5)
    meanGrade = max(meanGrade, 1)
    return meanGrade

def usersModel_predictionErrorsOverk(k_list, model, testSet=[], verbose=True):
    """
    Computes the prediction train error, and optionally test error, for different values of the parameter k using a user
    point of view based model.
    :param k_list: List of  values of k to use
    :param model: Users based model for making predictions
    :param testSet: Optional testSet, given as a sparse matrix whose non zero values are used as ground truth
    :param verbose: If set to True, status prints are made during the execution of the code
    :return: The list of train errors over k, and the (possibly empty) list of test errors over k
    """
    tr_errors = []
    te_errors = []

    for k in k_list:

        if verbose:
            print('k={}:'.format(k))

        count = 0
        mse = 0
        for u in range(model['nU']):
            if verbose and u % 100 == 0:
                print('User #{}'.format(u + 1))
            for f in range(model['nF']):
                if model['boolViewings'][f, u] == 1:
                    mse += (model['denseRatings'][f, u] - usersPrediction(f, u, k, model, verbose=verbose)) ** 2
                    count += 1
        tr_rmse = np.sqrt(mse / count)
        tr_errors.append(tr_rmse)
        if verbose:
            print("Train RMSE : {}".format(tr_rmse))

        if len(testSet) > 0:
            count = 0
            mse = 0
            nnz_rows, nnz_cols = testSet.nonzero()
            for f, u in list(zip(nnz_rows, nnz_cols)):
                mse += (testSet[f, u] - usersPrediction(f, u, k, model, verbose=verbose)) ** 2
                count += 1
            te_rmse = np.sqrt(mse / count)
            te_errors.append(te_rmse)
            if verbose:
                ("Test RMSE : {}".format(te_rmse))

    return tr_errors, te_errors

def filmsModel_predictionErrorsOverk(k_list, model, testSet=[], verbose=False):
    """
    Computes the prediction train error, and optionnaly test error, for different values of the parameter k using a
    film point of view based model.
    :param k_list: List of  values of k to use
    :param model: Films based model for making predictions
    :param testSet: Optional testSet, given as a sparse matrix whose non zero values are used as ground truth
    :param verbose: If set to True, status prints are made during the execution of the code
    :return: The list of train errors over k, and the (possibly empty) list of test errors over k
    """
    tr_errors = []
    te_errors = []

    for k in k_list:

        if verbose:
            print('k={}:'.format(k))

        count = 0
        mse = 0
        for u in range(model['nU']):
            if verbose and u % 100 == 0:
                print('User #{}'.format(u + 1))
            for f in range(model['nF']):
                if model['boolViewings'][f, u] == 1:
                    mse += (model['denseRatings'][f, u] - filmsPrediction(f, u, k, model, verbose=verbose)) ** 2
                    count += 1
        tr_rmse = np.sqrt(mse / count)
        tr_errors.append(tr_rmse)
        if verbose:
            print("Train RMSE : {}".format(tr_rmse))

        if len(testSet) > 0:
            count = 0
            mse = 0
            nnz_rows, nnz_cols = testSet.nonzero()
            for f, u in list(zip(nnz_rows, nnz_cols)):
                mse += (testSet[f, u] - filmsPrediction(f, u, k, model, verbose=verbose)) ** 2
                count += 1
            te_rmse = np.sqrt(mse / count)
            te_errors.append(te_rmse)
            if verbose:
                ("Test RMSE : {}".format(te_rmse))

    return tr_errors, te_errors

def create_prediction_file_usersModel(outputPath, testSet, model, k, verbose=True):
    """
    Creates a CSV output file containing predictions on the test set made by a user based model
    :param outputPath: Path of the file to create
    :param testSet: Sparse matrix, whose non zero entries indicate the predictions to make
    :param model: User based model to use for making the predictions
    :param k: Parameter k of the k nearest neighbors algorithm for making predictions
    :param verbose: If set to True, status prints are made during the execution of the code
    """
    with open(outputPath, 'w') as output:
        output.write('Id,Prediction\n')
        nnz_row, nnz_col = testSet.nonzero()
        for f, u in list(zip(nnz_row, nnz_col)):
            pred = usersPrediction(f, u, k, model, verbose=verbose)
            output.write('r{}_c{},{}\n'.format(f + 1, u + 1, pred))

def create_prediction_file_filmsModel(outputPath, testSet, model, k, verbose=False):
    """
    Creates a CSV output file containing predictions on the test set made by a film based model
    :param outputPath: Path of the file to create
    :param testSet: Sparse matrix, whose non zero entries indicate the predictions to make
    :param model: Film based model to use for making the predictions
    :param k: Parameter k of the k nearest neighbors algorithm for making predictions
    :param verbose: If set to True, status prints are made during the execution of the code
    """
    with open(outputPath, 'w') as output:
        output.write('Id,Prediction\n')
        nnz_row, nnz_col = testSet.nonzero()
        for f, u in list(zip(nnz_row, nnz_col)):
            pred = filmsPrediction(f, u, k, model, verbose=verbose)
            output.write('r{}_c{},{}\n'.format(f + 1, u + 1, pred))

def writeCSV(outputPath, ratings):
    """
    Creates a CSV output file from a ratings sparse matrix
    :param outputPath: Path of the file to create
    :param ratings: Sparse matrix, whose non zero entries indicate the entries to write into the CSV file
    """
    with open(outputPath, 'w') as output:
        output.write('Id,Prediction\n')
        nnz_row, nnz_col = ratings.nonzero()
        for f, u in list(zip(nnz_row, nnz_col)):
            pred = ratings[f, u]
            output.write('r{}_c{},{}\n'.format(f + 1, u + 1, pred))

def RMSE(ratings, groundTruth):
    """
    Computes the Root Mean Squared Error of a set of ratings given the ground truth:
    :param ratings: The under evaluation ratings sparse matrix
    :param groundTruth: The ground truth ratings sparse matrix
    :return: The RMSE of the ratings as compared to ground truth
    """
    nnz_row, nnz_col = ratings.nonzero()
    mse = 0
    count = 0
    for f, u in list(zip(nnz_row, nnz_col)):
        mse += (ratings[f, u] - groundTruth[f, u]) ** 2
        count += 1
    rmse = np.sqrt(mse / count)
    return rmse


import numpy as np
import matplotlib.pyplot as plt

def condition_points(points):
    """ Conditioning: Normalization of coordinates for numeric stability
    by substracting the mean and dividing by half of the component-wise
    maximum absolute value.
    Args:
        points: (l, 3) numpy array containing unnormalized homogeneous coordinates.

    Returns:
        ps: (l, 3) numpy array containing normalized points in homogeneous coordinates.
        T: (3, 3) numpy array, transformation matrix for conditioning
    """
    t = np.mean(points, axis=0)[:-1]
    s = 0.5 * np.max(np.abs(points), axis=0)[:-1]
    T = np.eye(3)
    T[0:2,2] = -t
    T[0:2, 0:3] = T[0:2, 0:3] / np.expand_dims(s, axis=1)
    ps = points @ T.T
    return ps, T

def enforce_rank2(A):
    """ Enforces rank 2 to a given 3 x 3 matrix by setting the smallest
    eigenvalue to zero.
    Args:
        A: (3, 3) numpy array, input matrix

    Returns:
        A_hat: (3, 3) numpy array, matrix with rank at most 2
    """

    U, d, V = np.linalg.svd(A)
    d[-1] = 0
    A_hat = U @ np.diag(d) @ V
    return A_hat

def compute_fundamental(p1, p2):
    """ Computes the fundamental matrix from conditioned coordinates.
    Args:
        p1: (n, 3) numpy array containing the conditioned coordinates in the left image
        p2: (n, 3) numpy array containing the conditioned coordinates in the right image

    Returns:
        F: (3, 3) numpy array, fundamental matrix
    """

    #
    # You code here
    #

    x1 = p1[:, 0:1]
    y1 = p1[:, 1:2]
    x2 = p2[:, 0:1]
    y2 = p2[:, 1:2]
    I = np.ones((x1.shape[0], 1))
    # Vorlesung l8-stereo Seite 21
    A = np.concatenate([x1*x2, y2*x1, x1, x2*y1, y1*y2, y1, x2, y2, I], axis=1)
    _, _, Vt = np.linalg.svd(A)
    F = Vt.T[:,-1].reshape((3,3))
    F = enforce_rank2(F)
    return F

def eight_point(p1, p2):
    """ Computes the fundamental matrix from unconditioned coordinates.
    Conditions coordinates first.
    Args:
        p1: (n, 3) numpy array containing the unconditioned homogeneous coordinates in the left image
        p2: (n, 3) numpy array containing the unconditioned homogeneous coordinates in the right image

    Returns:
        F: (3, 3) numpy array, fundamental matrix with respect to the unconditioned coordinates
    """

    u1, T1 = condition_points(p1)
    u2, T2 = condition_points(p2)
    F = compute_fundamental(u1, u2)
    # Vorlesung l8-stereo Seite 22
    F = (T1.T).dot(F.dot(T2))
    return F

def draw_epipolars(F, p1, img):
    """ Computes the coordinates of the n epipolar lines (X1, Y1) on the left image border and (X2, Y2)
    on the right image border.
    Args:
        F: (3, 3) numpy array, fundamental matrix
        p1: (n, 2) numpy array, cartesian coordinates of the point correspondences in the image
        img: (H, W, 3) numpy array, image data

    Returns:
        X1, X2, Y1, Y2: (n, ) numpy arrays containing the coordinates of the n epipolar lines
            at the image borders
    """

    n = p1.shape[0]
    P = np.concatenate([p1, np.ones((p1.shape[0], 1))], axis=1)
    l = P @ F.T
    # line coefficients: x^T * l = 0, with l = F*points = (a, b, c)^T
    # Vorlesung l8-stereo Seite 19
    a = l[:, 0]
    b = l[:, 1]
    c = l[:, 2]
    # first point of the line
    X1 = np.zeros(n)
    Y1 = (-c + a * X1) / b
    # last point of the line
    X2 = X1 + img.shape[1] - 1
    Y2 = -(c + a * X2) / b
    return X1, X2, Y1, Y2

def compute_residuals(p1, p2, F):
    """
    Computes the maximum and average absolute residual value of the epipolar constraint equation.
    Args:
        p1: (n, 3) numpy array containing the homogeneous correspondence coordinates from image 1
        p2: (n, 3) numpy array containing the homogeneous correspondence coordinates from image 2
        F:  (3, 3) numpy array, fundamental matrix

    Returns:
        max_residual: maximum absolute residual value
        avg_residual: average absolute residual value
    """

    n = p1.shape[0]
    residuals = []
    for i in range(n):
        residuals.append(np.abs(p1[i,:] @ F @ p2[i,:]))
    max_residual = np.max(residuals)
    avg_residual = np.mean(residuals)
    return max_residual, avg_residual

def compute_epipoles(F):
    """ Computes the cartesian coordinates of the epipoles e1 and e2 in image 1 and 2 respectively.
    Args:
        F: (3, 3) numpy array, fundamental matrix

    Returns:
        e1: (2, ) numpy array, cartesian coordinates of the epipole in image 1
        e2: (2, ) numpy array, cartesian coordinates of the epipole in image 2
    """

    # Vorlesung l8-stereo Seite 19
    V1, d, V2 = np.linalg.svd(F)
    e1 = V1[0:2, 2] / V1[2, 2]
    e2 = V2.T[0:2, 2] / V2[2, 2]
    return e1, e2


import numpy as np

def initializeRandom(dataArray, number_of_clusters, dim, dNo):
    # return k_initial_seeds
    initialseed = []
    for i in range(number_of_clusters):
        initialseed.append([])
        mx = np.max(dataArray, axis=0)
        mn = np.min(dataArray, axis=0)

        for j in range(len(mx)):
            initialseed[i].append(np.random.randint(mn[j], mx[j] + 1))
    reps = np.array(initialseed)
    return reps

def euclideanDistance(rep, data):
    # print(type(rep))
    # print(type(data))
    # return np.sum(np.sqrt(np.abs(rep**2-data**2)))
    # print(np.linalg.norm(rep - data))
    return np.linalg.norm(rep - data)

def manhattanDistance(rep, data):
    return np.sum(np.abs(rep - data), axis=0)

def clustering_k_means(datapointarray, k, reps, dNo):

    clusters = []

    for i in range(dNo):
        data = datapointarray[i]
        dist = []
        for j in range(int(k)):
            clusters.append([])
            # print(reps[j])
            dis = euclideanDistance(reps[j], data)
            dist.append(dis)
        # print("distance = ", dist)
        cNo = dist.index(min(dist))
        # print("Cluster assigned = ", cNo)
        clusters[cNo].append(data)

    return clusters

    # calculate distance from representative [call distance function]
    # assign to cluster
    # find cluster mean [call mean function]
    # set new cluster representative

def clustering_k_medians(datapointarray, k, reps, dNo):

    clusters = []

    for i in range(dNo):
        data = datapointarray[i]
        dist = []
        for j in range(int(k)):
            clusters.append([])
            dis = manhattanDistance(reps[j], data)
            dist.append(dis)

        cNo = dist.index(min(dist))
        clusters[cNo].append(data)

    return clusters

def findmeanofcluster(cluster):
    # cluster = np.delete(cluster, [0], axis=0)
    return np.mean(cluster, axis=0)

def findmedianofcluster(cluster):
    # return median
    return np.median(cluster, axis=0)


#count the inversions in an array of integers
#based on merge sort algorithm
import numpy as np
def merge(list1, list2):
    i = 0
    j = 0
    count = 0
    out = []
    for k in range(len(list1 + list2)):
        if list1[i] <= list2[j]:
            out.append(list1[i])
            i += 1
            if i == len(list1):
                out.extend(list2[j:])
                return out, count
        else:
            out.append(list2[j])
            j += 1
            count += len(list1) - i
            if j == len(list2):
                out.extend(list1[i:])
                return out, count
    return out, count

def divide(l):
    n = len(l)
    return l[0:n//2], l[n//2:]

def inv_count(l):
    if len(l) == 1:
        return l, 0
    else:
        list1, list2 = divide(l)
        #print(list1, list2)
        list1_sorted, count1 = inv_count(list1)
        list2_sorted, count2 = inv_count(list2)
        list_sorted, count_cross = merge(list1_sorted, list2_sorted)
        return list_sorted, count1 + count2 + count_cross

if __name__ == "__main__":
    data = np.loadtxt("./course1/week2/IntegerArray.txt")
    l = data.tolist()
    s, count = inv_count(l)
    print(count)


import numpy as np
import matplotlib.pyplot as plt

def minmax(t, u):
    """
    Compute all local minima and maxima of the function u(t),
    represented by discrete points in the arrays u and t.
    Return lists minima and maxima of (t[i],u[i]) extreme points.
    """
    minima = []; maxima = []
    for n in range(1, len(u)-1, 1):
        if u[n-1] > u[n] < u[n+1]:
            minima.append((t[n], u[n]))
        if u[n-1] < u[n] > u[n+1]:
            maxima.append((t[n], u[n]))
    return minima, maxima

def periods(extrema):
    """
    Given a list of (t,u) points of the maxima or minima,
    return an array of the corresponding local periods.
    """
    p = [extrema[n][0] - extrema[n-1][0]
         for n in range(1, len(extrema))]
    return np.array(p)

def amplitudes(minima, maxima):
    """
    Given a list of (t,u) points of the minima and maxima of
    u, return an array of the corresponding local amplitudes.
    """
    # Compare first maxima with first minima and so on
    a = [(abs(maxima[n][1] - minima[n][1]))/2.0
         for n in range(min(len(minima),len(maxima)))]
    return np.array(a)

def test_empirical_analysis():
    t = np.linspace(0, 6*np.pi, 1181)
    # Modulated amplitude and period
    u = np.exp(-(t-3*np.pi)**2/12.0)*np.cos(np.pi*(t + 0.6*np.sin(0.25*np.pi*t)))
    plt.plot(t, u, label='signal')
    plt.hold('on')
    minima, maxima = minmax(t, u)
    t_min = [ti for ti, ui in minima]
    t_max = [ti for ti, ui in maxima]
    u_min = [ui for ui, ui in minima]
    u_max = [ui for ui, ui in maxima]
    plt.plot(t_min, u_min, 'bo', label='minima')
    plt.plot(t_max, u_max, 'ro', label='maxima')
    plt.legend()

    plt.figure()
    p = periods(maxima)
    a = amplitudes(minima, maxima)
    plt.plot(range(len(p)), p, 'g--', label='periods')
    plt.hold('on')
    plt.plot(range(len(a)), a, 'y-', label='amplitudes')
    plt.legend()

    p_ref = np.array([
        1.48560059,  2.73158819,  2.30028479,  1.42170379,  1.45365219,
        2.39612999,  2.63574299,  1.45365219,  1.42170379])
    a_ref = np.array([
        0.00123696,  0.01207413,  0.19769443,  0.59800044,  0.90044961,
        0.96007725,  0.42076411,  0.08626735,  0.0203696 ,  0.00312785])
    p_diff = np.abs(p - p_ref).max()
    a_diff = np.abs(a - a_ref).max()
    tol = 1E-7
    assert p_diff < tol
    assert a_diff < tol

if __name__ == '__main__':
    test_empirical_analysis()
    plt.show()


import numpy as np
from PIL import Image
import matplotlib.pyplot as plt

# Open the image
img = np.array(Image.open('house.jpg')).astype(np.uint8)

# Sobel Operator
h, w, d = img.shape

# define filters
horizontal = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])  # s2
vertical = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]])  # s1

# define images with 0s
newgradientImage = np.zeros((h, w, d))

# offset by 1
for channel in range(d):
    for i in range(1, h - 1):
        for j in range(1, w - 1):
            horizontalGrad = (horizontal[0, 0] * img[i - 1, j - 1, channel]) + \
                             (horizontal[0, 1] * img[i - 1, j, channel]) + \
                             (horizontal[0, 2] * img[i - 1, j + 1, channel]) + \
                             (horizontal[1, 0] * img[i, j - 1, channel]) + \
                             (horizontal[1, 1] * img[i, j, channel]) + \
                             (horizontal[1, 2] * img[i, j + 1, channel]) + \
                             (horizontal[2, 0] * img[i + 1, j - 1, channel]) + \
                             (horizontal[2, 1] * img[i + 1, j, channel]) + \
                             (horizontal[2, 2] * img[i + 1, j + 1, channel])

            verticalGrad = (vertical[0, 0] * img[i - 1, j - 1, channel]) + \
                           (vertical[0, 1] * img[i - 1, j, channel]) + \
                           (vertical[0, 2] * img[i - 1, j + 1, channel]) + \
                           (vertical[1, 0] * img[i, j - 1, channel]) + \
                           (vertical[1, 1] * img[i, j, channel]) + \
                           (vertical[1, 2] * img[i, j + 1, channel]) + \
                           (vertical[2, 0] * img[i + 1, j - 1, channel]) + \
                           (vertical[2, 1] * img[i + 1, j, channel]) + \
                           (vertical[2, 2] * img[i + 1, j + 1, channel])

            # Edge Magnitude
            mag = np.sqrt(pow(horizontalGrad, 2.0) + pow(verticalGrad, 2.0))
            # Avoid underflow: clip result
            newgradientImage[i - 1, j - 1, channel] = mag

# now add the images r g and b
rgb_edge = newgradientImage[:,:,0] + newgradientImage[:,:,1] + newgradientImage[:,:,2]

plt.figure()
plt.title('RGBSobelEdge')
plt.imsave('RGBSobel_house.jpg', rgb_edge, cmap='gray')
plt.imshow(rgb_edge, cmap='gray')
plt.show()


import numpy as np

from TCalc.age_eye import age_to_eye_diameter

# This first set of functions are dependent ONLY on the telescope
# and don't require any information about the eyepiece
def focal_ratio(f_o,D_o):
    """Calculates the Focal Ratio for the telescope
    Args:
        f_o(float): Focal Length of Objective/Aperture (mm)
        D_o(float): Diameter of Objective/Aperture (mm)
    Returns:
        float: Focal Ratio/f-number (:math: `f_R`) of the telescope
    """
    return f_o/D_o

def dawes_lim(D_o):
    """Calulates the Reosultion of the telescope according to the Dawes Limit

    Args:
         D_o (float): Diameter of Objective/Aperture (mm)
    Returns:
        float: Resolving power of the telescope arroding to Dawes Limit (arc-sec)

    Note:
        This function is generally useful when you are doing filter-less observations. For operation with filters, check Resolution Power function.
    """
    return 115.8/D_o

def resolving_power(W,D_o):
    """Calulates the Reosultion of the telescope

    Args:
        W (float): Wavelength of light recieved (nm)
        D_o (float): Diameter of Objective/Aperture (mm)
    Returns:
        float: Resolving power of the telescope (:math: `P_R`) (arc-sec)

    Note:
        This function is generally useful when you are using filters. For filter-less observations, check Dawes Limit function.
    """
    return 0.2516616*(W/D_o)

def Min_magnification(D_o,D_eye=7,age=None):
    """Calulates the minimum usable magnification of the telescope
    Args:
         D_o (float): Diameter of Objective/Aperture (mm)
         D_eye (float): Diameter of Eyepiece (mm); default is 7 mm
    Returns:
        float: Minimum Usable Magnification (:math: `M_min`) of the telescope

    """
    if age is None:
        D_eye = D_eye
    else:
        D_eye = age_to_eye_diameter(age)

    return D_o/D_eye

def Max_magnification(D_o):
    """Calulates the maximum usable magnification of the telescope
    Args:
         D_o (float): Diameter of Objective/Aperture (mm)
    Returns:
        float: Maximum Usable Magnification (:math: `M_max`) of the telescope

    Note:
        The maximum usable magnification also depends upon seeing.
    """
    return 2*D_o

def Min_eyepiece(f_o,M_max):
    """Calulates the minimum focal length of eyepiece you can use on the telescope
    Args:
         f_o (float): Focal length of Objective/Aperture (mm)
         M_max (float): Maximum usable magnification
    Returns:
        float: The minimum focal length of eyepiece (:math: `f_{e-min}`) you can use with your telescope

    """
    return f_o/M_max

def Max_eyepiece(f_R, D_eye=7, age=None):
    """Calulates the maximum focal length of eyepiece you can use on the telescope
    Args:
         D_eye (float): Diameter of Eyepiece (mm); default is 7 mm
         f_R (float): Focal Ratio/f-number of telescope
    Returns:
        float: The maximum focal length of eyepiece (:math: `f_{e-max}`) you can use with your telescope

    """
    if age is None:
        D_eye = D_eye
    else:
        D_eye = age_to_eye_diameter(age)

    return f_R*D_eye

def Lmag_limit(D_o):
    """Calculates the magnitude of the faintest star which can be seen from the telescope

    Args:
         D_o (float): Diameter of Objective/Aperture (mm)

    Returns:
        float: Limiting Magnitude (LM) of telescope
    """
    return 2 + 5*np.log10(D_o)

# These next functions depend on the properties of the telescope
# and the eyepiece, and no other information
def magnification(f_o,f_e):
    """Calculates the Angular Magnification for the telescope
    Args:
        f_o(float): Focal Length of Objective/Aperture (mm)
        f_e(float): Focal Length of Eyepiece (mm)
    Returns:
        float: Visual Magnification (M) of the telescope
    """
    return f_o/f_e

def true_fov(M, fov_e=50):
    """Calulates the True Field of View (FOV) of the telescope & eyepiece pair
    Args:
        fov_e (float): FOV of eyepiece; default 50 deg
        M (float): Magnification of Telescope
    Returns:
        float: True Field of View (deg)

    """
    return fov_e/M

def exit_pupil(f_e,f_R):
    """Calulates the Exit Pupil of the telescope & eyepiece pair
    Args:
        f_e (float): Focal Length of eyepiece (mm)
        f_R (float): Focal Ratio of telescope
    Returns:
        float: Diameter of Exit Pupil(:math: `D_EP`) (mm)

    Note:
        For optimal usage, :math: `D_EP \approx 2-3 mm` is the optimum range for maximizing the resolving power of the eye
    """
    return f_e/f_R

def surface_brightness(D_EP):
    """Calculates the surface brightness of the object as seen from the telescope

    Args:
         D_o (float): Diameter of Exit Pupil (mm)

    Returns:
        percentage: Surface Brightness (SB)
    """
    return 2*(D_EP**2)


import numpy as np
import pymc as pm
from matplotlib import pyplot as plt

# The parameters to be inferred. We only know them here because we are synthesising the data.
true_alpha = 10
true_beta = 50

num_flashes = 5000

# Generate the angles
true_thetas = np.random.uniform(low=-0.5*np.pi, high=0.5*np.pi, size=num_flashes)

# Generate the x coordinates of the flashes along the coastline
data = true_alpha + true_beta * np.tan(true_thetas)

alpha = pm.Normal("alpha", 0, 1.0/50**2)
beta = pm.Exponential("beta", 1.0/100)

# @pm.stochastic(name="obs", dtype=np.float64, observed=True)
# def obs(value=data, alpha=alpha, beta=beta):
#     return np.sum(np.log(beta) - np.log(np.pi) - np.log(beta**2 + (alpha-value)**2))

@pm.observed(name="obs", dtype=np.float64)
def obs(value=data, alpha=alpha, beta=beta):
    return np.sum(np.log(beta) - np.log(np.pi) - np.log(beta**2 + (alpha-value)**2))

model = pm.Model([alpha, beta, obs])

mcmc = pm.MCMC(model)
mcmc.sample(40000, 10000, 1)

alpha_samples = mcmc.trace("alpha")[:]
beta_samples = mcmc.trace("beta")[:]

# histogram of the samples:

plt.hist(alpha_samples, normed = True)
plt.show()

plt.hist(beta_samples, normed = True)
plt.show()


# An example from Wikipedia
# https://en.wikipedia.org/wiki/Gradient_descent

import time
import math;
import numpy as np

def G(x):

  x1 = x[0]
  x2 = x[1]
  x3 = x[2]
  return np.array([
    3*x1 - math.cos(x2*x3) - 3/2,
    4*x1**2 - 625*x2**2 + 2*x2 -1,
    math.exp(-x1*x2) + 20*x3 + (10*math.pi - 3)/3
  ])

def J(x):
  x1 = x[0]
  x2 = x[1]
  x3 = x[2]
  return np.array([
    [3, math.sin(x2*x3)*x3,  math.sin(x2*x3)*x2],
    [8*x1, -1250*x2 + 2, 0],
    [-x2*math.exp(-x1*x2), -x1*math.exp(-x1*x2), 20]
  ])

def error(x):
  return 0.5 * ((x**2 - 1)**2)

def solve():
  x = np.array([5, 5, 5])

  for i in range(100):
    g = G(x)
    j = J(x)

    delta = np.linalg.solve(j, -g)
    x = x + delta

  g = G(x)
  print(x)
  print(np.dot(g, g))

if __name__ == '__main__':

  start_time = time.time()

  solve()
  print(f'{(time.time() - start_time) * 1000} ms')


import numpy as np

# data = "3,4,3,1,2"
data = "1,4,3,3,1,3,1,1,1,2,1,1,1,4,4,1,5,5,3,1,3,5,2,1,5,2,4,1,4,5,4,1,5,1,5,5,1,1,1,4,1,5,1,1,1,1,1,4,1,2,5,1,4,1,2,1,1,5,1,1,1,1,4,1,5,1,1,2,1,4,5,1,2,1,2,2,1,1,1,1,1,5,5,3,1,1,1,1,1,4,2,4,1,2,1,4,2,3,1,4,5,3,3,2,1,1,5,4,1,1,1,2,1,1,5,4,5,1,3,1,1,1,1,1,1,2,1,3,1,2,1,1,1,1,1,1,1,2,1,1,1,1,2,1,1,1,1,1,1,4,5,1,3,1,4,4,2,3,4,1,1,1,5,1,1,1,4,1,5,4,3,1,5,1,1,1,1,1,5,4,1,1,1,4,3,1,3,3,1,3,2,1,1,3,1,1,4,5,1,1,1,1,1,3,1,4,1,3,1,5,4,5,1,1,5,1,1,4,1,1,1,3,1,1,4,2,3,1,1,1,1,2,4,1,1,1,1,1,2,3,1,5,5,1,4,1,1,1,1,3,3,1,4,1,2,1,3,1,1,1,3,2,2,1,5,1,1,3,2,1,1,5,1,1,1,1,1,1,1,1,1,1,2,5,1,1,1,1,3,1,1,1,1,1,1,1,1,5,5,1"

fish = np.zeros(shape=(9, 1), dtype=np.int64)
for i in np.array(data.split(",")).astype(int):
    fish[i] += 1

numdays = 256
for i in range(0, numdays):
    newfish = np.copy(fish)
    newfish[0:8] = fish[1:9]
    newfish[8] = fish[0]
    newfish[6] += fish[0]
    fish = newfish
    print(f"afer day {i+1} {np.sum(fish)}")


import matplotlib.pyplot as plt
import numpy as np
from numpy.linalg import inv
import csv

def read_csv(filename):
    with open(filename, 'rt') as csv_file:
        points = []
        buffer = csv.reader(csv_file)
        for row in buffer:
            points.append([float(x) for x in row])
    return points

def compute_polynoms(points):
    a = points[1]
    n = a.size

    h = np.zeros(n - 1)
    for i in range(n - 1):
        h[i] = points[0][i + 1] - points[0][i]

    u = np.zeros((n, n))
    u[0][0] = 1
    u[n - 1][n - 1] = 1
    for i in range(1, n - 1):
        u[i][i - 1] = h[i - 1]
        u[i][i] = 2 * (h[i - 1] + h[i])
        u[i][i + 1] = h[i]

    v = np.zeros(n)
    for i in range(1, n - 1):
        v[i] = (3 * (a[i + 1] - a[i]) / h[i]) - \
            (3 * (a[i] - a[i - 1]) / h[i - 1])

    c = inv(u).dot(v)

    d = np.zeros(n - 1)
    b = np.zeros(n - 1)

    for i in range(n - 1):
        d[i] = (c[i + 1] - c[i]) / (3 * h[i])
        b[i] = (a[i + 1] - a[i]) / h[i] - ((2 * c[i] + c[i + 1]) * h[i]) / 3

    return a, b, c, d

def spline_interpolation(points, x):
    a, b, c, d = compute_polynoms(points)
    k = -1
    for i in range(points[0].size - 1):
        if points[0][i] <= x and x <= points[0][i + 1]:
            k = i

    return a[k] + (b[k] * (x - points[0][k])) + (c[k] * (x - points[0][k]) ** 2) + (d[k] * (x - points[0][k]) ** 3)

if(__name__ == "__main__"):
    points = np.array(read_csv("points.csv"))
    interval = np.linspace(min(points[0]), max(points[0]), 5000)
    interpolation = [spline_interpolation(points, x) for x in interval]
    plt.plot(interval, interpolation)
    plt.plot(points[0], points[1], 'o')
    plt.show()


import math
import numpy as np

from .utils import pad_image

def G(x, sigma):
    """
Calculates a gaussian kernel"""
    return ((1 / (2 * math.pi * pow(sigma, 2))) *
            math.exp(-(pow(x, 2) / (2 * pow(sigma, 2)))))

def spatial_component(n, sigma):
    """
Calculates the spatial gaussian kernel
Arguments:
    n     -- filter size
    sigma -- variance"""
    center = int((n - 1) / 2) # index
    def dist(x, y):
        return math.sqrt(pow(x - center, 2) + pow(y - center, 2))
    matrix = np.zeros([n, n])
    for i in range(n):
        for j in range(n):
            matrix[i, j] = G(dist(i, j), sigma)
    return matrix

def range_component(region, n, sigma):
    """
Calculates the range gaussian kernel of a region of the image
Arguments:
    region -- region of the image to use
    n      -- filter size
    sigma  -- variance"""
    center = int((n - 1) / 2)
    pixel = region[center, center]
    matrix = np.zeros([n, n])
    for i in range(n):
        for j in range(n):
            Ii = region[i, j]
            matrix[i, j] = G(Ii - pixel, sigma)
    return matrix

def filter(image, n, sigma_s, sigma_r):
    """
Bilateral Filter
Arguments:
    image -- input image
    n     -- size of the filter
    σs    -- space variance
    σr    -- range variance"""

    gs = spatial_component(n, sigma_s)

    padding_needed = math.floor(n / 2)

    result = np.empty_like(image)
    working_area = pad_image(image, padding_needed)

    for i in range(image.shape[0]):
        for j in range(image.shape[1]):
            region = working_area[i:i + n, j:j + n]
            gr = range_component(region, n, sigma_r)
            w = gr * gs
            Wp = np.sum(w)
            If = np.sum(w * region)
            result[i, j] = If / Wp

    return result


from sympy import *

# https://docs.sympy.org/latest/install.html
# conda install sympy
# conda install mpmath

# Sobre matrices: https://docs.sympy.org/latest/tutorial/matrices.html

def eval_mai(G1, G2, C1, C2):
  return Matrix([[s*C1+G1, -G1,        -s*C1,    0   ], \
                 [-G1,     G1+G2+s*C2, -G2,  -   s*C2], \
                 [-s*C1,   -G2,         s*C1+G2, 0   ], \
                 [0,       -s*C2,       0,       s*C2]])

def eval_f_in_s(f, attributes, ww):
  f_eval = []
  for w in ww:
    attributes['s'] = w*I
    f_eval.append(f.evalf(subs=attributes))
  return f_eval

# Para mejorar la impresion
init_printing()

# Defino simbolos
G1 = Symbol('G1')
G2 = Symbol('G2')
C1 = Symbol('C1')
C2 = Symbol('C2')
s = Symbol('s')

# Cargo la MAI
print('Matriz admitancia indefinida para el T puenteado.')
print(eval_mai(G1, G2, C1, C2))

# Computamos la transferencia de tension.
# Recordamos la expresion:
# $ V_{14}^{34} = (-1)^{-1-4-3-4} \frac{Y_{14}^{34}}{Y_{14}^{14}} $

# Cargo la matriz definida del numerador.
Y_14_34 = eval_mai(G1, G2, C1, C2)
Y_14_34.row_del(3)
Y_14_34.row_del(2)
Y_14_34.col_del(3)
Y_14_34.col_del(0)

# Cargo la matriz definida del denominador.
Y_14_14 = eval_mai(G1, G2, C1, C2)
Y_14_14.row_del(3)
Y_14_14.row_del(0)
Y_14_14.col_del(3)
Y_14_14.col_del(0)

# Computo la transferencia
V_14_34 = (-1.0)**(-1.0-4.0-3.0-4.0) * Y_14_34.det() / Y_14_14.det()
V_14_34 = cancel(V_14_34)
print('La transferencia de tension es:')
print(V_14_34)


import numpy as np

class LinearRegression(object):
    def __init__(self, learn_rate):
        self.weights = np.array([])
        self.learn_rate = learn_rate

    def _compute_gradient(self, X, Y):
        return (X.T.dot(X.dot(self.weights) - Y)) * 2 / len(Y)

    def fit(self, X, Y):
        intercept = np.ones((len(X), 1))
        X = np.append(intercept, X, 1)

        self.weights = np.zeros((len(X.T)))
        grad = np.ones((len(X.T), 1))

        while np.linalg.norm(grad) > 0.001:
            grad = self._compute_gradient(X, Y)
            self.weights -= grad * self.learn_rate

    def predict(self, X):
        intercept = np.ones((len(X), 1))
        X = np.append(intercept, X, 1)

        return X.dot(self.weights)

class Ridge(LinearRegression):
    def __init__(self, learn_rate, alpha):
        super().__init__(learn_rate)
        self.alpha = alpha

    def _compute_gradient(self, X, Y):
        return (((X.T.dot(X.dot(self.weights) - Y)) * 2
                + self.weights * self.alpha * 2)
                / len(Y))


# +++++++++++++++
# PROBLEM DISCRIPTION
# +++++++++++++++
#A googol (10^100) is a massive number: one followed by one-hundred zeros;
# 100^100 is almost unimaginably large: one followed by two-hundred zeros.
# Despite their size, the sum of the digits in each number is only 1.
#
# Considering natural numbers of the form, a^b, where a, b < 100, what is the maximum digital sum?
#
# +++++++++++++++
import numpy as np
import math

def digitalSum(n):
    nDigits = [int(i) for i in str(n)]
    sum = 0
    for digit in nDigits:
        sum += digit
    return sum

maxDigitalSum = 0

for a in range(1,100):
    for b in range(1,100):
        num = a ** b
        sum = digitalSum(num)
        if sum > maxDigitalSum:
            maxDigitalSum = sum
            print a, "^" , b, " with digital sum of ", maxDigitalSum, " is the new max"

digitalSum(num)
print "Fin"


from statistics import mean
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import style
import random

style.use('fivethirtyeight')

#xs = np.array([1,2,3,4,5,6], dtype=np.float64)
#ys = np.array([5,4,6,5,6,7], dtype=np.float64)

def create_dataset(hm, variance, step=2, correlation=False):
    val = 1
    ys = []
    for i in range(hm):
        y = val + random.randrange(-variance, variance)
        ys.append(y)
        if correlation and correlation == 'pos':
            val +=step
        elif correlation and correlation == 'neg':
            val -=step
    xs = [i for i in range(len(ys))]
    return np.array(xs, dtype=np.float64), np.array(ys, dtype=np.float64)

def best_fit_slope_and_intercept(xs,ys):
    m = ( ((mean(xs) * mean(ys)) - mean(xs*ys)) /
        ((mean(xs)*mean(xs)) - mean(xs*xs)))

    b = mean(ys) - m*mean(xs)

    return m, b

def squared_error(ys_orig, ys_line):
    return sum((ys_line-ys_orig)**2)

def coefficient_of_determination(ys_orig,ys_line):
    y_mean_line = [mean(ys_orig) for y in ys_orig]
    squared_error_regr = squared_error(ys_orig, ys_line)
    squared_error_y_mean = squared_error(ys_orig, y_mean_line)
    return 1 - (squared_error_regr/ squared_error_y_mean)

xs, ys = create_dataset(40, 10, 2, correlation='pos')

m ,b= best_fit_slope_and_intercept(xs,ys)

regression_line = [(m*x)+b for x in xs]

predict_x = 8
predict_y = (m*predict_x)+b

r_squared = coefficient_of_determination(ys, regression_line)
print(r_squared)

plt.scatter(xs,ys)
plt.plot(xs, regression_line)
plt.scatter(predict_x,predict_y, s=100, color='g')

plt.show()


# coding: utf-8
'''
激活函数
用来处理一些非线性的东西
'''

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

x = np.linspace(-7, 7, 180)  # 生成-7~7的等间距180个点

# 激活函数的原始实现(直接套用数学公式定义)
def sigmoid(input):
    '''
    sigmoid: S型函数
    形状如y = 1/e^x
  '''
    y = [1 / float(1 + np.exp(-x)) for x in input]
    return y

def relu(input):
    '''
    当x<=0时 y=0,
    当x>0时 y=x的运算
  '''
    y = [x * (x > 0) for x in input]
    return y

def tanh(input):
    '''tan 函数'''
    y = []
    for x in input:
        y1 = np.exp(x) - np.exp(-x)
        y2 = float(np.exp(x) + np.exp(-x))
        y.append(y1 / y2)
    return y

def softPlus(input):
    y = [np.log(1 + np.exp(x)) for x in input]
    return y

# 使用tf定义
tf_sigmoid = tf.nn.sigmoid(x)
tf_relu = tf.nn.relu(x)
tf_tanh = tf.nn.tanh(x)
tf_softPlus = tf.nn.softplus(x)
# 运算每个的结果
sess = tf.Session()
y_sigmoid, y_relu, y_tanh, y_softPlus = sess.run(
    [tf_sigmoid, tf_relu, tf_tanh, tf_softPlus])
sess.close()

# 绘制图像
plt.subplot(221)
plt.plot(x, y_sigmoid, label="sigmoid")

plt.subplot(222)
plt.plot(x, y_relu, label="relu")

plt.subplot(223)
plt.plot(x, y_tanh, label="tanh")

plt.subplot(224)
plt.plot(x, y_softPlus, label="softPlus")

plt.show()


import numpy as np

class KNearestNeighbour():
    def computeEuclideanDistance(self, a, b):
        return np.sqrt(np.sum(np.power(a-b, 2), axis=1))

    def getEuclideandistanceBetweenEachTrainAndTestRow(self, train, test):
        """
        For each test row we compute the euclidean distance to each train row
        :param train: the train set
        :param test: the test set
        :return: result, where number of rows = number of rows in test
                and number of columns = number of rows in train
        """
        result = np.empty((test.shape[0], train.shape[0]))

        for i in range(test.shape[0]):
            # here I'm doing implicit broadcasting: test[i] is a row but train is a matrix
            result[i] = self.computeEuclideanDistance(test[i], train)

            if i > 0:
                percentage_done = i/test.shape[0]*100
                if percentage_done % 5 == 0:
                    print("KNearestNeighbour: Evaluated " + str(i) + " test entries. That is " + str(percentage_done))

        return result

    def predict(self, train_x, train_y, test_x):
        """
        :param train_x: features of the train set, one row is one example
        :param train_y: label of the train set, one row is one example
        :param test_x: features of the test set, on row is one example
        :return: test_predicted: the predicted labels for each of the test set rows
        """

        m = self.getEuclideandistanceBetweenEachTrainAndTestRow(train_x, test_x)

        # get minimum per column
        closest_train_X_rows = np.argmin(m, axis=1)

        test_predicted = np.empty(test_x.shape[0])

        for i in range(test_x.shape[0]):
            test_predicted[i]=train_y[closest_train_X_rows[i]]

        return test_predicted


import numpy as np

class KMeans(object):
	def __init__(self, inputs, clusters):
		self.inputs = np.array(inputs)
		if self.inputs.ndim == 1:
			self.inputs = self.inputs.reshape(self.inputs.shape[0], 1)
		self.shuffle()
		self.test_cases = self.inputs.shape[0]
		self.num_clusters = clusters
		self.clusters = self.inputs[np.random.choice(self.test_cases, clusters, replace=False), :]
		self.cluster_dist = np.zeros((self.test_cases, self.num_clusters))

	def shuffle(self):
		np.random.shuffle(self.inputs)

	def calculate_cluster_distances(self):
		for i in range(self.num_clusters):
			self.cluster_dist[:, i] = np.sqrt(np.sum((self.inputs - self.clusters[i])**2, axis=1))

	def assign_data_points(self):
		assigned_cluster = np.where(self.cluster_dist.argsort(axis=1) == 0)[1]
		for i in range(self.num_clusters):
			points = self.inputs[assigned_cluster == i]
			self.clusters[i] = points.sum(axis=0)/points.shape[0]

	def train(self):
		while True:
			old_clusters_norm = np.linalg.norm(self.clusters, axis=1)
			self.shuffle()
			self.calculate_cluster_distances()
			self.assign_data_points()
			new_clusters_norm = np.linalg.norm(self.clusters, axis=1)

			if np.isnan(self.clusters).any() == True:
				# If one of the clusters is stuck at nan then start whole
				# process again from begining.
				self.clusters = self.inputs[np.random.choice(
					self.test_cases, self.num_clusters, replace=False), :]
			elif (old_clusters_norm == new_clusters_norm).all() == True:
				break

	def assign_clusters(self, inputs):
		cluster_dist = np.zeros((inputs.shape[0], self.num_clusters))
		for i in range(self.num_clusters):
			cluster_dist[:, i] = np.sqrt(np.sum((inputs - self.clusters[i])**2, axis=1))
		assigned_clusters = np.where(cluster_dist.argsort(axis=1) == 0)[1]
		return assigned_clusters, cluster_dist


import numpy as np

def copy(x):
    if isinstance(x,np.ndarray):
        return x
    if isinstance(x, int) or isinstance(x, float):
        return x
    cp = []
    for i in x:
        cp.append(copy(i))
    return cp

def hierarchical_cluster(inp, beta=0.0, min_c=None, max_c=None):
    """
    clusters the data with hierarchical clustering
    :param inp: data to cluster
    :param beta: a parameter indicating the significance of the size of two joined clusters. a larger beta would mean
    that clusters with a large size are not likely to be joined with other clusters with great size
    default to 0: cluster size is insignificant
    :param min_c: minimum amount of clusters
    :param max_c: maximum amount of clusters
    :return: an array of integers, each indicating the cluster of the input parameter with whom they share an index
    """
    prev_score = 0
    clusters = [[i] for i in inp]
    steps = [{"clusters": clusters}]

    if min_c is None:
        min_c = 1

    def min_distance(x, y):
        m = np.Inf
        for i in x:
            for j in y:
                d = np.sum((i-j)**2)
                if d < m:
                    m = d
        return np.sqrt(m)

    def stamp_mark(x, y):
        return min_distance(x, y) + (beta*min(len(x), len(y)))

    while len(clusters) > min_c:
        clusters = copy(clusters)
        join1, join2 = 0, 0
        min_score = np.Inf
        for i in xrange(len(clusters)):
            for j in xrange(i+1, len(clusters)):
                score = stamp_mark(clusters[i], clusters[j])
                if score < min_score:
                    min_score = score
                    join1 = i
                    join2 = j
        clusters[join1] += clusters[join2]
        del clusters[join2]
        steps.append({"clusters": clusters})
        steps[-2]['stamp_mark'] = min_score-prev_score
        prev_score = min_score

    steps = steps[:-1]

    if max_c is not None:
        steps = steps[max_c:]

    max_score = 0
    step = steps[0]['clusters']
    for i in steps:
        if i['stamp_mark'] > max_score:
            max_score = i['stamp_mark']
            step = i['clusters']

    def cluster_of(x):
        for i in xrange(len(step)):
            for j in step[i]:
                if (j == x).all():
                    return i

    return [cluster_of(x) for x in inp]

def main():
    data = np.array([[136,345],[109,362],[139,373],[129,70],[105,84],[121,84],[394,246],[386,257],[400,256]])
    clusters = hierarchical_cluster(data)
    print clusters

if __name__ == '__main__':
    main()

My Machine Learning Kit/unsupervised/HierarchicalCluster.py


# -*- coding: utf-8 -*-
import numpy as np

def average_difference(data: np.ndarray) -> float:
    """计算平均偏差（不常用）

    :param data: 要计算的数据
    :return: 返回平均偏差

    Example:

    >>> average_difference(np.array([0.2,0.3,0.2,0.1]))
    0.05000000000000001
    """
    x_ = data.mean()
    return np.abs(data - x_).mean()

def std(data: np.ndarray, ddof: int = 1) -> float:
    """ 计算修正的样本标准差

    :param data: 要求标准差的数据
    :param ddof: 修正值，除法分母为n - ddof，默认为1
    :return: 标准差

    >>> std(np.array([0.2,0.2,0.2,0.2]))
    0.0
    """
    return data.std(ddof=ddof)

def variation_coefficient(data: np.ndarray, ddof: int = 1) -> float:
    """计算样本的变异系数

    :param data:
    :param ddof:
    :return:

    >>> variation_coefficient(np.array([41.24,41.27,41.23,41.26]))
    0.00044260408687289926
    """
    return std(data,ddof) / data.mean()


import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
import statsmodels.api as sm

#Load the PCE and UMCSENT datasets
df = pd.read_csv(filepath_or_buffer='UMCSENT_PCE.csv', header=0, index_col=0,
                 infer_datetime_format=True, parse_dates=['DATE'])
#Set the index frequency to 'Month-Start'
df = df.asfreq('MS')

#Plot both time series
fig = plt.figure()
fig.suptitle('% Chg in Personal Consumption Expenditure')
df['PCE_CHG'].plot()
plt.show()
fig = plt.figure()
fig.suptitle('% Chg in U. Michigan Consumer Sentiment Index')
df['UMCSENT_CHG'].plot()
plt.show()

#build and train the MSDR model
msdr_model = sm.tsa.MarkovRegression(endog=df['PCE_CHG'], k_regimes=2,
    trend='c', exog=df['UMCSENT_CHG'], switching_variance=True)
msdr_model_results = msdr_model.fit(iter=1000)

#print model training summary
print(msdr_model_results.summary())

df_r = pd.read_csv('JHDUSRGDPBR.csv', header=0, index_col=0,
                   infer_datetime_format=True, parse_dates=['DATE'])

fig, axes = plt.subplots(3)
ax = axes[0]
ax.plot(df.index, df['PCE_CHG'])
ax.set(title="% Chg in Personal Consumption Expenditure")

ax = axes[1]
ax.plot(df.index, msdr_model_results.smoothed_marginal_probabilities[0])
ax.set(title="Smoothed probability of regime 0")

ax = axes[2]
ax.plot(df.index, msdr_model_results.smoothed_marginal_probabilities[1])
ax.plot(df_r.index, df_r['JHDUSRGDPBR'])
ax.set(title="Smoothed probability of regime 1 super-imposed on GDP based "
             "recession indicator (Orange)")

plt.show()


#!/usr/bin/env python
import os
import sys
import argparse
import string
import warnings
import shutil

import math, random
import numpy as np

#from jodl gayatin
#Imagine four points randomly selected on a surface of a sphere. These points form the vertices of an imaginary tetrahedron. What is the probability that the tetrahedron contains the center of the sphere? Imagine four points randomly selected on a surface of a sphere. These points form the vertices of an imaginary tetrahedron. What is the probability that the tetrahedron contains the center of the sphere?

#from https://stackoverflow.com/questions/9600801/evenly-distributing-n-points-on-a-sphere

def fibonacci_sphere(samples=1,randomize=True):
    rnd = 1.
    if randomize:
        rnd = random.random() * samples

    points = []
    offset = 2./samples
    increment = math.pi * (3. - math.sqrt(5.));

    for i in range(samples):
        y = ((i * offset) - 1) + (offset / 2);
        r = math.sqrt(1 - pow(y,2))

        phi = ((i + rnd) % samples) * increment

        x = math.cos(phi) * r
        z = math.sin(phi) * r

        points.append(np.array([x,y,z]))

    return points

#from https://stackoverflow.com/questions/25179693/how-to-check-whether-the-point-is-in-the-tetrahedron-or-not
def sameside(v1,v2,v3,v4,p):
    normal = np.cross(v2-v1, v3-v1)
    result =  np.dot(normal, v4-v1) * np.dot(normal, p-v1) > 0
    return result

def tetraCoord(A,B,C,D):
    v1 = B-A ; v2 = C-A ; v3 = D-A
    # mat defines an affine transform from the tetrahedron to the orthogonal system
    mat = np.concatenate((np.array((v1,v2,v3,A)).T, np.array([[0,0,0,1]])))
    # The inverse matrix does the opposite (from orthogonal to tetrahedron)
    M1 = np.linalg.inv(mat)
    return(M1)

def pointInsideT(v1,v2,v3,v4,p):
    # Find the transform matrix from orthogonal to tetrahedron system
    try:
        M1=tetraCoord(v1,v2,v3,v4)
        # apply the transform to P
        p1 = np.append(p,1)
        newp = M1.dot(p1)
        # perform test
        return(np.all(newp>=0) and np.all(newp <=1) and sameside(v2,v3,v4,v1,p))
    except np.linalg.linalg.LinAlgError:
        return False

def parseArgs():
    """ Simply parse the arguments """
    parser = argparse.ArgumentParser(description = 'Check if apk size is out of cellular download limit.')
    args = parser.parse_args()
    return args

def main():
    # parse arguments
    args = parseArgs()
    #checkAndroidApkSize(args)
    numPointsOnSphere = 50
    points = fibonacci_sphere(numPointsOnSphere, False)
    numInside = 0;
    numTests = 0;
    origin = np.array([0,0,0])
    for p1 in points:
        for p2 in points:
            for p3 in points:
                for p4 in points:
                    numTests += 1
                    if pointInsideT( p1, p2, p3, p4, origin):
                        numInside += 1;

    print "numPointsOnSphere = ", numPointsOnSphere
    print "NumTests = ", numTests
    print "NumInside = ", numInside
    print "Probabiliy=", float(numInside) / float(numTests)

if __name__ == "__main__":
    main()


import numpy as np

# Random Number Generator - Uniform Distribution
def U():
        return np.random.rand()

# Integrated Function
def f(x):
    return np.exp(x)

if __name__ == "__main__":
    # Number of Random Numbers (Preferably use big number since this method converges rather slowly)
    N = 100000

    # Limits of Integration
    a, b = 0, 10

    # It is not actually mandatory to be the exact max{f(x)}
    f_max = f(b)

    # "Volume" of Integration
    V = (b-a) * f_max

    Hits = 0

    for i in range(N):
        x = (b-a) * U() + a
        y = f_max * U()

        if y <= f(x):
            Hits += 1

    # Probalitiy to Hit
    p = Hits/N

    # Integral Computed with Hit-or-Miss Method
    I = V * p

    # Integral Error
    δI = V/np.sqrt(N) * np.sqrt(p-p**2)

    print(f'Ι ± δI = {I:.2f} ± {δI:.2f}')


"""
Function to find the gradient descent on a set of data points divided into 4 classes
Algorithm:

a(k + 1) = a(k) − η(k)∇J(a(k))
a is weight vector
η is learning rate.
J is the hypothesis function
∇ - gradient
k is the iteration or the sample number
"""
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

def grab_data(file_path):
    data_df = pd.read_csv(file_path)
    data_features = np.zeros([10,8])
    i=0
    for col in data_df.columns:
        data_features[:,i] = data_df[col]
        i+=1
    train_features, labels = create_dataset(data_features)

    return [train_features, labels]

def create_dataset(data):

    cls_1 = data[:, 0:2]
    cls_2 = data[:, 2:4]
    features = np.concatenate((cls_1,cls_2))
    labels = np.array([1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0])
    return features, labels

def split_data():
    pass

def optimize_weight_vector(*, method=0, features=None):
    operation(method=method, epochs=60000, tr_label=1, train_set=features, lr=0.0001)

def sigmoid_activation(x):
    # compute and return the sigmoid activation value for a
    # given input value
    return 1.0 / (1 + np.exp(-x))

def operation(*,method=0,epochs=600, tr_label=1, train_set=None, lr=0.1):
    """
    a(k + 1) = a(k) − η(k)∇J(a(k))
    ∇J(a) = y(k)

    :param epochs:
    :param train_set:
    :param tr_label:
    :param lr:
    :return:
    """
    loss_history = []
    training_data, labels = train_set[0], train_set[1]

    training_data_points = augument_train_vector(training_data)

    # initialize weights
    weights = np.random.uniform(size=(training_data_points.shape[1],))

    for each_epoch in np.arange(0, epochs):
        pred = find_hypothesis(training_data_points.dot(weights))
        # pred = training_data_points.dot(weights)
        error = pred - labels
        loss = np.sum(error ** 2)
        loss_history.append(loss)
        print("[INFO] epoch #{}, loss={:.7f}".format(each_epoch + 1, loss))

        gradient = training_data_points.T.dot(error) / training_data_points.shape[0]
        if method == 0:
            weights = gradient_descent(weights, gradient, lr)
        else:
            weights = perceptron_algo(weights, gradient, lr)

    plot_results(method, weights, training_data, labels,loss_history, epochs)

def gradient_descent(weights, gradient, lr):
    """
    a(k + 1) = a(k) − η(k)∇J(a(k))
    ∇J(a) = y(k)
    :param weights:
    :param gradient:
    :param lr:
    :return:
    """"""
    # """

    weights += -(lr * gradient)
    return weights

def perceptron_algo(weights, gradient, lr):
    """
    a(k + 1) = a(k) + η(k)∇J(a(k))
    ∇J(a) = y(k)
    :param weights:
    :param gradient:
    :param lr:
    :return:
    """"""
    # """

    weights += (lr * gradient)
    return weights

def find_hypothesis(x):
    """
    using sigmoid function for the hypothesis
       J(a(k)) = dot(a'Y) > 0.5 -> clssified
                 dot(a'Y) < 0.5 -> Misclassified
    :return:
    """
    # compute and return the sigmoid activation value for a
    # given input value
    return 1.0 / (1 + np.exp(-x))

def augument_train_vector(data_points):
    """
    augument input/train vector x into y i.e. [x1, x2, x3 ...... xn, 1]

    :return:
    """
    return np.c_[np.ones((data_points.shape[0])), data_points]

def plot_results(method, W, X,labels, loss, epochs):
    Y = (-W[0] - (W[1] * X)) / W[2]
    if method == 0:
        graph = "Gradient descent: Classification between Class 1 and Class 2"
    else:
        graph = "Perceptron Algo: Classification between Class 1 and Class 2"

    # plot the original data along with our lin e of best fit
    plt.figure()
    plt.title(graph)
    plt.scatter(X[:, 0], X[:, 1], marker="o", c=labels)
    plt.plot(X, Y, "r-")

    # construct a figure that plots the loss over time
    fig = plt.figure()
    plt.plot(np.arange(0, epochs), loss)
    fig.suptitle("Training Loss")
    plt.xlabel("Epoch #")
    plt.ylabel("Loss")
    plt.show()

def classify():
    pass

def main():
    " method = 0 for gradient descent and 1 for perceptron"
    features = grab_data('sample_gd.csv')
    # split_data()
    optimize_weight_vector(method=0, features=features)
    # classify()

if __name__ == '__main__':
    main()


import numpy as np

def euclidian_distance(a: np.ndarray, b: np.ndarray) -> np.ndarray:
    diff = a - b
    elms_pow = diff ** 2
    elms_sum = np.sum(elms_pow, axis=1)
    return np.sqrt(elms_sum)


#!/usr/bin/env python

'''
Adapted from geopriv4j (Java) packaged: https://dl.acm.org/doi/pdf/10.1145/3403896.3403968 (Fan and Gunja, 2020);
/*
 * In this method we will be generating a new location (z) with a probability (p) that
 * reduces as the distance increase within a certain radius (r).
 *
 * As in Geopriv4j, this algorithm is based on the paper by Andrés, Miguel E., et al.
 * "Geo-indistinguishability: Differential privacy for location-based systems."
 * Proceedings of the 2013 ACM SIGSAC conference on Computer & communications
 * security. 2013. https://doi.org/10.1145/2508859.2516735
'''

import numpy as np
#Define constant earth radius
earth_radius = 6.3781e6

#Convert degrees to radians
def rad_of_deg(ang):
	return np.radians(ang)

#Convert radians to degrees
def deg_of_rad(ang):
	return np.degrees(ang)

#Define Lambda distribution
def LambertW(x):
	#Min diff decides when the while loop ends
	min_diff = 1e-10
	if (x == -1 / np.e):
		return -1
	elif ((x < 0) and (x > -1/np.e)):
		q = np.log(-x)
		p = 1
		while (abs(p-q) > min_diff):
			p = (q * q + x / np.exp(q)) / (q + 1)
			q = (p * p + x / np.exp(p)) / (p + 1)
		#determine the precision of the float number to be returned
		return (np.round(1000000 * q) / 1000000)
	elif (x == 0):
		return 0
	else:
		return 0

def inverseCumulativeGamma(epsilon,z):
	x = (z - 1) / np.e
	return -(LambertW(x) + 1)/epsilon

def alphaDeltaAccuracy(epsilon,delta):
	return inverseCumulativeGamma(epsilon,delta)

def expectedError(epsilon):
	return 2/epsilon

def addPolarNoise(epsilon,lng,lat):
	#Random number in [0, 2*pi)
	theta = np.random.rand()*np.pi*2
	#Random number in [0,1)
	z = np.random.rand()
	r = inverseCumulativeGamma(epsilon,z)
	return addVectorToPos(lng,lat,r,theta)

def addVectorToPos(lng,lat,distance,angle):
	ang_distance = distance / earth_radius
	lat1 = rad_of_deg(lat)
	lon1 = rad_of_deg(lng)
	lat2 = np.arcsin(np.sin(lat1)*np.cos(ang_distance) + np.cos(lat1)*np.sin(ang_distance)*np.cos(angle))
	lon2 = lon1 + np.arctan2(np.sin(angle)*np.sin(ang_distance)*np.cos(lat1),np.cos(ang_distance)-np.sin(lat1)*np.sin(lat2))
	lon2 = (lon2 + 3*np.pi) % (2*np.pi) - np.pi #Normalize to -180 to +180
	latnew = deg_of_rad(lat2)
	lngnew = deg_of_rad(lon2)
	return lngnew,latnew

#Geoindistinguishability algorithm
class LaplaceAlgorithm:
	#Init
	def __init__(self,epsilon=0.1):
		self.epsilon = epsilon

	def generate(self,lng,lat):
		#print(str(lng)+','+str(lat)+','+str(self.epsilon))
		return addPolarNoise(self.epsilon, lng, lat)


import numpy as np
import math

def normalize(vector):
    ''' Return the input vector divided by its norm. The resulting vector is of magnitude 1
    The input vector must a list. '''
    magn = magnitude(vector)
    v = np.asarray(vector, dtype=float).copy()
    v /= magn

    if isinstance(vector, np.ndarray):
        return v
    else:
        return v.tolist()

def magnitude(vector):
    sqr_sum = 0
    for val in vector:
        sqr_sum += val**2
    return math.sqrt(sqr_sum)

def cartesian2spherical(xyz):
    x = xyz[0]
    y = xyz[1]
    z = xyz[2]

    radial = magnitude(xyz)
    azimuth = math.atan2(y, x)
    elevation = math.atan2(z, magnitude((x, y)))
    return [radial, azimuth, elevation]

def spherical2cartesian(rae):
    radial = rae[0]
    azimuth = rae[1]
    elevation = rae[2]

    x = radial*math.cos(azimuth)*math.cos(elevation)
    y = radial*math.sin(azimuth)*math.sin(elevation)
    z = radial*math.sin(elevation)
    return [x, y, z]

def axisAngle_to_Rmat(axis, angle):
    """
    Return the rotation matrix associated with counterclockwise rotation about
    the given axis by theta radians.
    http://stackoverflow.com/a/6802723
    """
    axis = np.asarray(axis)
    axis = axis/math.sqrt(np.dot(axis, axis))
    a = math.cos(angle/2.0)
    b, c, d = -axis*math.sin(angle/2.0)
    aa, bb, cc, dd = a*a, b*b, c*c, d*d
    bc, ad, ac, ab, bd, cd = b*c, a*d, a*c, a*b, b*d, c*d
    return np.array([
        [aa+bb-cc-dd, 2*(bc+ad), 2*(bd-ac), 0.0],
        [2*(bc-ad), aa+cc-bb-dd, 2*(cd+ab), 0.0],
        [2*(bd+ac), 2*(cd-ab), aa+dd-bb-cc, 0.0],
        [0.0, 0.0, 0.0, 1.0]
        ])

def clean_taitbryan_angle(thetas):
    """
    tait-bryan angle's space is not uniform and we must be carful when theta_y tends to +-pi/2.
    Indeed, when theta_y is +-pi/2, there is an infinite number of combination (theta_x, theta_z) describing the same rotaton.
    In that case, we choose to set theta_z to 0, and compute theta_x accordingly.
    :param thetas: [theta_x, theta_y, theta_z]
    :return: corrected thetas: [theta_x, theta_y, theta_z]
    """
    thetas = thetas + [] # deep copy to avoid bad surprises
    R = thetas_to_Rmat(thetas)
    thetas = Rmat_to_thetas(R)

    pi = np.pi
    thetas[1] = min(pi, max(-pi, thetas[1]))  # clip theta_y in range [-pi/2, pi/2] because of rounding errors

    # most of the time we won't fall in the if case
    if pi / 2 - abs(thetas[1]) < 1e-6:
        thetas[0] -= math.copysign(thetas[2], thetas[1])
        thetas[2] = 0

    # force theta_x and theta_z to stay in range [-pi, pi)
    thetas[0] = (thetas[0] + pi) % (2 * pi) - pi
    thetas[2] = (thetas[2] + pi) % (2 * pi) - pi
    return thetas

def Rmat_to_axisAngle(R):
    #todo test this function
    """
    :param R:
    :return:
    """
    angle = math.acos(( R[0,0] + R[1,1] + R[2,2] - 1)/2)
    x = (R[2,1] - R[1,2]) / math.sqrt((R[2,1] - R[1,2])**2 + (R[0,2] - R[2,0])**2 + (R[1,0] - R[0,1])**2)
    y = (R[0,2] - R[2,0]) / math.sqrt((R[2,1] - R[1,2])**2 + (R[0,2] - R[2,0])**2 + (R[1,0] - R[0,1])**2)
    z = (R[1,0] - R[0,1]) / math.sqrt((R[2,1] - R[1,2])**2 + (R[0,2] - R[2,0])**2 + (R[1,0] - R[0,1])**2)
    axis = [x,y,z]
    return axis, angle

def thetas_to_Rmat(thetas, homogenous=True):
    """
    Compute the rotation matrix R, given the Tait-Bryan angle vector thetas = [theta_x, theta_y, theta_z]
    The rotation is applied in this order: R = Rz * Ry * Rx
    :param thetas: the 3 angles [theta_x, theta_y, theta_z] in radian
    :param homogenous: if homogenous=True R will be (4x4), if homogenous=False R will be (3x3)
    :return: Rotation matrix R
    """

    cx = math.cos(thetas[0])
    sx = math.sin(thetas[0])
    cy = math.cos(thetas[1])
    sy = math.sin(thetas[1])
    cz = math.cos(thetas[2])
    sz = math.sin(thetas[2])

    Rx = np.matrix([[1.0, 0.0, 0.0],
                    [0.0, cx, -sx],
                    [0.0, sx, cx]])

    Ry = np.matrix([[ cy, 0.0,  sy],
                    [0.0, 1.0, 0.0],
                    [-sy, 0.0,  cy]])

    Rz = np.matrix([[ cz, -sz, 0.0],
                    [ sz,  cz, 0.0],
                    [0.0, 0.0, 1.0]])
    R = Rz * Ry * Rx

    if homogenous:
        R = np.hstack((R, np.array([[0], [0], [0]])))
        R = np.vstack((R, np.array([0,0,0,1])))
    return R

def quaternion_to_Rmat(qx, qy, qz, qw):
    #TODO test this function
    """
    found at: http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToMatrix/index.htm
    :param qx:
    :param qy:
    :param qz:
    :param qw:
    :return:
    """
    R = np.matrix([
        [1 - 2*qy**2 - 2*qz**2,	    2*qx*qy - 2*qz*qw,  	2*qx*qz + 2*qy*qw,  0],
        [    2*qx*qy + 2*qz*qw,	1 - 2*qx**2 - 2*qz**2, 	    2*qy*qz - 2*qx*qw,  0],
        [    2*qx*qz - 2*qy*qw,	    2*qy*qz + 2*qx*qw, 	1 - 2*qx**2 - 2*qy**2,  0],
        [                    0,                     0,                      0,  1]
    ])
    return R

def copysign0(a, b):
    """
    same as math.copysign but handle the 0 case
    """
    if b == 0:
        return 0
    else:
        return math.copysign(a, b)

def Rmat_to_quaternion(R):
    """
    code founded at: http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm
    :param R:
    :return:
    """
    # the use of max is just a safeguard against rounding error
    qw = math.sqrt(max(0, 1 + R[0,0] + R[1,1] + R[2,2])) / 2
    qx = math.sqrt(max(0, 1 + R[0,0] - R[1,1] - R[2,2])) / 2
    qy = math.sqrt(max(0, 1 - R[0,0] + R[1,1] - R[2,2])) / 2
    qz = math.sqrt(max(0, 1 - R[0,0] - R[1,1] + R[2,2])) / 2

    # copysign takes the sign from the second term and sets the sign of the first without altering the magnitude
    qx = copysign0(qx, R[2,1] - R[1,2])
    qy = copysign0(qy, R[0,2] - R[2,0])
    qz = copysign0(qz, R[1,0] - R[0,1])
    return qx, qy, qz, qw

def Rmat_to_thetas(R):
    """
    Extract Tait-Bryan angles from rotation matrix
    :param R:
    :return: len-3 list of theta angles x, y, z in radian
    """
    singular_y = math.sqrt(R[0, 0] * R[0, 0] + R[1, 0] * R[1, 0])
    th_y = math.atan2(-R[2, 0], singular_y)  # similar to asin(-R[2, 0])
    singular = singular_y < 1e-6

    if not singular:
        # most of the time we fall in the non-singulare case
        th_x = math.atan2(R[2, 1], R[2, 2])
        th_z = math.atan2(R[1, 0], R[0, 0])
    else:
        # When th_y tends to pi/2 or -pi/2, then x and z rotation are mixed together leading to an infinity of solution.
        # So we set th_z to 0 to have a single solution
        th_x = math.atan2(-R[1, 2], R[1, 1])
        th_z = 0

    thetas = [th_x, th_y, th_z]
    return thetas

def thetas_to_quaternion(thetas):
    # TODO: enhance the doc
    """
    go through Rmat
    :param thetas:
    :return:
    """
    R = thetas_to_Rmat(thetas)
    qx, qy, qz, qw = Rmat_to_quaternion(R)
    return qx, qy, qz, qw

def quaternion_to_thetas(qx, qy, qz, qw):
    # TODO: write the doc for this taitbryan function
    R = quaternion_to_Rmat(qx, qy, qz, qw)
    thetas = Rmat_to_thetas(R)
    return thetas

def translation_mat4(translation_vector):
    T = np.identity(4, dtype='float')
    T[0, 3] = translation_vector[0]
    T[1, 3] = translation_vector[1]
    T[2, 3] = translation_vector[2]
    return T

def mat4_dot_vec3(Mat4, vec3):
    """
    INPUTS
        Mat4: A 4x4 transformation matrix
        vec3: A 3x1 vector containing coordinates [x,y,z]
    OUTPUT
        A 3x1 vector
    """

    # Turn the vec3 into a vec4 by appending a 1 to it (resulting in a 4x1 vector)
    vec4 = np.asarray(list(vec3)+[1.0]).copy()
    # compute the dot product between Mat4 and vec4 resulting in a (4x1) vector
    res4 = np.dot(Mat4, vec4)
    # return the first 3 entries of the vector
    return res4[:3].tolist()

def plot(fig, ax, tag_xyz, locator_xyz, locator_pyr, tag_local_xyz, tag_local_rae):

    xs = [tag_xyz[0]]
    ys = [tag_xyz[1]]
    zs = [tag_xyz[2]]
    ax.scatter(xs, ys, zs, c='r', marker='o')


import numpy as np

def sigmoid(val):
    if val < 0:
        return 1 - 1/(1 + np.exp(val))
    else:
        return 1/(1 + np.exp(-val))

def v_func(val):
    return abs(val/(np.sqrt(1 + val*val)))

def u_func(val, alpha=2, beta=1.5):
    return abs(alpha * np.power(abs(val), beta))

def get_trans_function(shape):
    if (shape.lower() == 's'):
        return sigmoid

    elif (shape.lower() == 'v'):
        return v_func

    elif(shape.lower() == 'u'):
        return u_func

    else:
        print('\n[Error!] We don\'t currently support {}-shaped transfer functions...\n'.format(shape))
        exit(1)


# -*- coding: utf-8 -*-
"""
Created on Wed Jan  9 14:11:39 2019

@author: Tian
"""

import numpy as np

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

def sigmoidPrime(x):
    v = sigmoid(x)
    return v * (1 - v)

def sigmoidDerivative(x, y):
    return y * (1 - y)

def tanh(x):
    return np.tanh(x);

def tanhPrime(x):
    t=tanh(x);
    return 1-t^2;

def tanhDerivative(x, y):
    return 1-y^2;

def relu(x):
    return np.maximum(x, 0);

def reluPrime(x):
    return np.where(x>0, 1, 0)

def reluDerivative(x, y):
    return np.where(y, 1, 0)

def softplus(x):
	return np.log1p(np.exp(x))

def softplusPrime(x):
	return sigmoid(x)

def softplusDerivative(x, y):
	return sigmoid(y)


import numpy as np
from scipy.ndimage import convolve

# Few stencils for first and second order derivatives.

def d1(u, axis, dx=1.0e0, n_points=5, mode="wrap"):
    """
    First order derivative of a two dimensional array.

    axis     : 'x' or 'y'
    n_points : Number of points to use in the stencil.
    mode     : scipy convolve mode. Use 'wrap' for periodic.
    """

    dx_inv = dx
    if n_points == 5:
        stencil = (dx_inv / 12.0e0) * np.array([[-1, 8, 0, -8, 1]])
    elif n_points == 3:
        stencil = (dx_inv / 2.0e0) * np.array([[-1, 0, 1]])
    if axis == "x":
        return convolve(u, stencil, mode=mode)
    if axis == "y":
        return convolve(u, stencil.T, mode=mode)

def d2(u, axis, dx=1.0e0, n_points=5, mode="wrap"):
    """
    Second order derivative of a two dimensional array.

    axis     : 'x' or 'y'
    n_points : Number of points to use in the stencil.
    mode     : scipy convolve mode. Use 'wrap' for periodic.
    """
    dx_inv = dx
    if n_points == 5:
        stencil = (dx_inv ** 2 / 12.0e0) * np.array([[-1, 16, -30, 16, -1]])
    elif n_points == 3:
        stencil = (dx_inv ** 2) * np.array([[1, -2, 1]])
    if axis == "x":
        return convolve(u, stencil, mode=mode)
    if axis == "y":
        return convolve(u, stencil.T, mode=mode)


import numpy as np
import time
from scipy.integrate import odeint
from math import *
import matplotlib.pyplot as plt

def MassSpringDamper(state,t,F, mode='critically_damped'):
	x = state[0]
	xd = state[1]
	if mode=='oscillations':
		m = 1.0; k = 2; b = 0 # undapmped: oscillations
	elif mode=='underdamped':
		m = 1.0; k = 2; b = 2*sqrt(m*k)-2 # underdamped
	elif mode=='overdamped':
		m = 1.0; k = 2; b = 2*sqrt(m*k)+2 # overdamped
	else:
		m = 1.0; k = 2; b = 2*sqrt(m*k) # critically damped
	# print mode+", damping ratio="+str(b/2/sqrt(m*k))
	xdd = -(b/m)*xd - (k/m)*x + F/m
	return [xd, xdd]

y0 = [np.pi - 0.1, 0.0]
M = 10
t = np.linspace(0, 4*pi, 101)
modes = ['underdamped', 'overdamped', 'critically_damped', 'oscillations']

plt.rcParams.update({'font.size': 30})
plt.figure(figsize=(20,20))
plt.grid()
plt.title('Impedance modeles')
plt.xlabel('t')
plt.ylabel('x(t)')
for mode in modes:
	# differential equation solution
	sol = odeint(MassSpringDamper, y0, t, args=(M,mode,))
	plt.plot(t, sol[:, 0], label=mode, linewidth=4)
	# plt.plot(t, sol[:, 1], 'g', label='omega(t)')
plt.legend(loc='best')

# close windows if Enter-button is pressed
plt.draw()
plt.pause(1)
raw_input('Hit Enter to close')
plt.close('all')


import numpy as np
from scipy.stats import norm
from math import log, sqrt, exp, pi

class Opcion:
    def __init__(self,s,k,r,sigma,T, kind):
        """ s = spot, k = strike , r = tasa (decimal), sigma = volatilidad (decimal), T = dias """
        self.s = float(s)
        self.k = float(k)
        self.r = float(r)
        self.sigma = float(sigma)
        self.T = float(T/360)
        self.kind = kind
        self.d1 =( log(self.s/self.k) + ( self.r + (self.sigma**2 /2) )*self.T ) /   (self.sigma * sqrt(self.T) )
        self.d2 = self.d1 - (self.sigma * sqrt(self.T))
        self.discount = exp(-(self.r) * self.T)

    def value(self):
        if (self.kind == "call"):
            call = self.s*norm.cdf(self.d1) - self.k*self.discount*norm.cdf(self.d2)
            return(call)
        else:
            put = self.k*self.discount*norm.cdf(-self.d2) - self.s*norm.cdf(-self.d1)
            return(put)

    def delta(self):
        if (self.kind == "call"):
            return(norm.cdf(self.d1))
        else:
            return(norm.cdf(self.d1)-1)

    def gamma(self):
        gamma = exp((-self.d1 ** 2)/2)  / (self.s * self.sigma * sqrt(self.T) * 2 * pi)
        return(gamma)

    def vega(self):
        vega = self.s * sqrt(self.T) * exp((-self.d1 ** 2)/2) / sqrt(2 * pi)
        return(vega)

    def valuation_curve(self):
    #Establish uper and lower limit for range
        if self.s > self.k:
            liminf = 0.9 * self.s
            limsup = 1.1 * self.k
        else:
            liminf = 0.9 * self.k
            limsup = 1.1 * self.s
        rango = np.arange(start = liminf, stop = limsup, step = self.s/100).tolist()
        valores = []

        for i in rango:
            x = Opcion(s = i,k = self.k ,r = self.r ,sigma = self.sigma, T = self.T, kind = self.kind)
            valores.append(x.value())
        val_cur = [rango, valores]
        return val_cur

a = Opcion(s = 50.6 ,k = 50 ,r = 0.05 ,sigma = 0.39 ,T = 3, kind = "put")
print(a.value())
print(a.delta())
print(a.gamma())
print(a.vega())
res = a.valuation_curve()
import matplotlib.pyplot as plt
plt.plot(res[0], res[1])
plt.show()


import numpy as np

def f(y):
    return -50*y

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

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

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

    return x

def implict_Euler(x, func, ini):
    """ Solve the ordinary equation y' = -50y, y(0)=100

    Args:
        x: list/ndarray, the split of the interval
        func: the function f(y)
        ini: double, the initial value for IVP

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

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

    return y

if __name__ == '__main__':
    h = [0.1, 0.05, 0.01, 0.001]
    for h0 in h:
        print(f"The numerical solution of ODE for h = {h0} is as follows:")
        inte = [0, 20*h0]
        x = split(inte, h0)
        y = implict_Euler(x, f, 100)
        for item in zip(x, y):
            print(f"{item[0]:.3f}\t{item[1]:.16f}")


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

cmap = 'copper'

def plot_faces(ims,imshape,cmap='gray'):
    fig = plt.figure(figsize=(18,3),constrained_layout=True, dpi=100)
    gs  = fig.add_gridspec(1,len(ims))
    for i,im in enumerate(ims):
        ax = fig.add_subplot(gs[0,i],xticks=[],yticks=[])
        ax.imshow(im.reshape(imshape),cmap=cmap)

def plot_pca_projection(
        im_face,
        mean_face,
        eigenfaces,
        coeffs,
        n_components=None,
        k_components=None,
        imshape=(0,0),
        cmap='gray'
    ):

    n_components = len(eigenfaces) if n_components is None else n_components
    k_components = n_components if k_components is None else k_components

    def plot_component(fig,pos,im,text='',left_align=True,plus=0,l2_norm=None):
        ax = fig.add_subplot(gs[pos[0],pos[1]],xticks=[],yticks=[])
        ax.imshow(im.reshape(imshape),cmap=cmap)
        ax.set_title(text,pad=-0.1)
        if left_align:
            ax.text(-40,103,'+',fontsize=12,alpha=plus)
        if l2_norm is not None:
            ax.text(45,220,f'{l2_norm:.1f}',fontsize=12)

    fig = plt.figure(figsize=(18,3),constrained_layout=True, dpi=100)
    gs  = fig.add_gridspec(2,n_components+5)
    fig.set_constrained_layout_pads(w_pad=0, h_pad=0, hspace=0, wspace=0)

    # Mean axes
    plot_component(fig,[0,2],im=mean_face,text=f'$mean\,face$',left_align=False)
    plot_component(fig,[1,2],im=mean_face,text=r'$coeffs\longrightarrow $',left_align=False)

    # Other axes
    proj_face = mean_face
    for k in range(n_components):
        eig_face = eigenfaces[k]
        plot_component(fig,[0,k+3],im=eig_face,text=f'$eigen\,face_{k+1}$')

        if k in range(k_components):
            coeff = coeffs[k]
            proj_face += eig_face * coeff
            l2_norm = np.sum(np.square(im_face - proj_face)) / len(im_face)
            plot_component(fig,[1,k+3],im=proj_face,text=f'{coeffs[k]:4.1f}',plus=1,l2_norm=l2_norm)

    # Left & Right axes
    axL = fig.add_subplot(gs[:, :2],xticks=[],yticks=[]); axL.set_title('Original')
    axR = fig.add_subplot(gs[:,-2:],xticks=[],yticks=[]); axR.set_title('Projection')

    axL.imshow(im_face.reshape(imshape),cmap=cmap)
    axR.imshow(proj_face.reshape(imshape),cmap=cmap)

    axL.text(45,203,f'$L2 \, norms \longrightarrow$',fontsize=12)
    axR.text(75,203,f'{l2_norm:.1f}',fontsize=12)
    plt.show()

# %% [markdown]
# ### Training Data
# In this study, we have 10 face samples of different identities from **Yale Face Database B**. These image samples are stored in gray-scale pixel intensities with $(192,168)$ dimensions. Below routine reads and flattens them to construct a training set **X** with a shape of $(10,32256)$. Original faces are displayed in **copper color-map**.

# %%
img_vector_list = []
path_main = "./face_Yale/"
img_path_list = [os.path.join(path_main,path_img) for path_img in os.listdir(path_main)]
for img_path in img_path_list:
    img = mpimg.imread(img_path)
    imshape = img.shape # (192,168)
    img_vector = img.flatten() # (32256,)
    img_vector_list.append(img_vector)
X = np.array(img_vector_list) # (10,32256)
plot_faces(X,imshape=imshape,cmap=cmap)

# %% [markdown]
# ### PCA & Eigenfaces
# PCA implementation of Sklearn library have three basic parameters: **n_components**, **whiten** and **svd_solver**. <br>
#
# If **n_components** is in $[0,1]$ interval; then, algorithm returns you the components which explains the dataset variance by that specified value. If it is not specified ($None$), we can reach all components to make further analysis. However, it is good to know that evaluated maximum number of components are limited to the smallest size of the data:
# $$n\_components = min(n\_samples,n\_features) = 10 $$
#
# **n_whiten** is such a normalization that principal components are multiplied by the square root of $n\_samples$ and divided by singular values. This process makes uncorrelated outputs to have unit variances. Since, whitening changes (eliminates some information) the training data, keep it $False$ for our toy example. (False by default)
#
# **svd_solver** determines how the eigen-vectors are calculated. In our case, we have large feature set ($n\_features = 32256$) and this makes the calculation of eigen-value and eigen-vectors not efficient. Choosing $svd\_solver = auto$ will result in use of Randomized Singular Value Decomposition method (Halko et al) to solve this problem. It is an intelligent way to aproximate the first principal components quickly.
#

# %%
from sklearn.decomposition import PCA

pca = PCA(n_components=None,whiten=False,svd_solver='auto').fit(X)
eigenfaces = pca.components_
coeffs = pca.transform(X)

# %% [markdown]
# Evaluated PCA components are stored in **eigenfaces** variable. **coeffs** is the weights for each eigenface to transform the original face to projection space.
# $$coeffs = X \cdot eigenfaces^T $$
# Here are the first 10 eigenfaces of the training set, respectively:
#

# %%
plot_faces(pca.components_,imshape=imshape,cmap=cmap)

# %% [markdown]
# Because $9$ eigenfaces can cover all the training samples $10^{th}$ eigen face seems like it does not exist.

# %% [markdown]
# ### How to choose $k$ eigenfaces?
# Explained variance is a measurement of importance for each component to represent the data. One component's explained variance is approximately equal to the ratio of corresponding eigen-value to the sum of all eigen-values. Therefore, we can arrive an expression of cumulative sum by combining $k$ components:
# $$ Explained \, Var(k) = \frac{\sum_i^k \lambda_i}{ \sum_{i}^N \lambda_i } $$
# where N is the total number of components and $k\leq N$.
#
# In the below figure, horizontal axis is the number of principal components (eigenfaces) and vertical axis is the explained variance approximation. Inspecting the cumulative explained variance (orange plot) can lead us to choose $k=5$ for $80$% explained variance desire.
#
# Again, $10^{th}$ eigenface has no impact to the data representation and the cumulative explained variance succeed to reach the top $1.0$ with $9$ eigenfaces.

# %%
fig = plt.figure(figsize=(10,5))
plt.plot(np.arange(1,11,1),pca.explained_variance_ratio_,marker='x',\
    label='explained variance by $i^{th}$ eigenface')
plt.plot(np.arange(1,11,1),np.cumsum(pca.explained_variance_ratio_),marker='o',\
    label='cumulative explained variance')
plt.xticks(np.arange(0,11,1)); plt.yticks(np.arange(0,1.1,0.1))
plt.xlabel('PCA components (eigenfaces)'); plt.ylabel('explained variance')
plt.grid(); plt.legend(); plt.show()

# %% [markdown]
# ### Projection and Error
# Eigenfaces and their identity-based coefficients are evaluated, already. Projection is simply adding the weighted summation of these eigenfaces and coefficients to the mean face of the training samples. This progress is visualized based on an illustration made by [Jake VanderPlas](https://jakevdp.github.io/PythonDataScienceHandbook/06.00-figure-code.html#Digits-Pixel-Components). My implementation is available at the bottom of this report.
#
# The original face is located in the most left of the below figure. The eigenfaces are followed the mean face in the first row of the 2D face array. Between the first and second row of the 2D face array, coefficients correspond to related eigenfaces are placed. In the second row, there are projections with $i=\{1,...,k\}$ eigenfaces, where $i$ increased by column and $k$ is the specified eigenface number in the last section. The values under these projections are their L2 norms to the original face. $k^{th}$ column projection starting after the mean face is the final projection and also displayed in the most right of the figure.

# %%
k = 5
for i in range(1,k+1):
    plot_pca_projection(
        im_face=X[8],
        mean_face=np.mean(X,axis=0),
        eigenfaces=eigenfaces,
        coeffs=coeffs[8],
        n_components=9,
        k_components=i,
        imshape=imshape,
        cmap=cmap
        )

# %% [markdown]
# ### Utilizing PCA for Storage Purpose
# Let's make an approximate analysis on a question of how much memory space we are required to store the **10** faces.
# Say, **S** is the memory allocation for one face of the dataset **X**. Thus, in total **10S** space is required without PCA.
# In this application, we have found that $k=5$ eigenfaces performed acceptable results to represent the **X**. Therefore, we might store **5** eigenfaces, **1** mean face and relatively small coefficients matrix to reconstruct the **X**. If memory need for coefficients is denoted as **eps** total memory need can be evaluated to **6S+eps**.
#
# Finally, we infer from the assumptions above that PCA has provided **almost 40%** gain in terms of memory space.

# %% [markdown]
# ### Projections for Each Identity

# %%
for id in range(len(X)):
    plot_pca_projection(
        im_face=X[id],
        mean_face=np.mean(X,axis=0),
        eigenfaces=eigenfaces,
        coeffs=coeffs[id],
        n_components=9,
        k_components=None,
        imshape=imshape,
        cmap=cmap
    )