from scipy.special import roots_jacobi, jacobi, gamma
import numpy as np
import torch

# GAUSS-LOBATTO-JACOBI QUADRATURE RULE
##################################################

def jacobi_evaluation(n, alpha, beta, x):
    '''Recursive generation of the Jacobi polynomial of order n.

    Args:
        n (int): Order of the Jacobi Polynomial, :math:`\ge 0`.
        alpha (float): Alpha parameter of the Jacobi polynomial, :math:`> -1`.
        beta (float): Beta parameter of the Jacobi polynomial, :math:`> -1`.
        x (np.ndarray): Array to evaluate.

    Returns:
        y (np.ndarray): A numpy array of the Jacobi polynomial evaluated at x.

    Example:
        >>> n, alpha, beta = 1, 0, 0
        >>> x = np.linspace(-1., 1., 5)
        >>> y = jacobi_evaluation(n, alpha, beta, x)
        >>> print(y)
        [-1.  -0.5  0.   0.5  1. ]

    '''
    x = np.array(x)
    return jacobi(n, alpha, beta)(x)

def gauss_lobatto_jacobi_weights(q, alpha=0, beta=0):
    '''Returns the roots and weights of the Gauss-Lobatto-Jacobi quadrature rule.

    Args:
        q (int): Number of roots for the quadrature rule, :math:`\ge 1`.
        alpha (float, optional): Alpha parameter of the Jacobi polynomial, :math:`> -1`. Defaults to 0.
        beta (float, optional): Beta parameter of the Jacobi polynomial, :math:`> -1`. Defaults to 0.

    Returns:
        roots (np.ndarray): roots of the Jacobi polynomial.
        weights (np.ndarray): weights of the quadrature rule.

    Example:

        >>> roots, weights = gauss_lobatto_jacobi_weights(5, 0, 0)
        >>> print(roots, weights)
        [-1.         -0.65465367  0.          0.65465367  1.        ] [0.1        0.54444444 0.71111111 0.54444444 0.1       ]

    '''
    w = []
    x = roots_jacobi(q - 2, alpha + 1, beta + 1)[0]
    if alpha == 0 and beta == 0:
        w = 2 / ((q - 1) * (q) * (jacobi_evaluation(q - 1, 0, 0, x) ** 2))
        wl = 2 / ((q - 1) * (q) * (jacobi_evaluation(q - 1, 0, 0, -1) ** 2))
        wr = 2 / ((q - 1) * (q) * (jacobi_evaluation(q - 1, 0, 0, 1) ** 2))
    else:
        w = 2 ** (alpha + beta + 1) * gamma(alpha + q) * gamma(beta + q) / ((q - 1) * gamma(q)
                                                                            * gamma(alpha + beta + q + 1) * (
                                                                                jacobi_evaluation(q - 1, alpha, beta,
                                                                                x) ** 2))
        wl = (beta + 1) * 2 ** (alpha + beta + 1) * gamma(alpha + q) * gamma(beta + q) / ((q - 1)
                                                                                * gamma(q) * gamma(
                    alpha + beta + q + 1) * (jacobi_evaluation(q - 1, alpha, beta, -1) ** 2))
        wr = (alpha + 1) * 2 ** (alpha + beta + 1) * gamma(alpha + q) * gamma(beta + q) / ((q - 1)
                                                                                * gamma(q) * gamma(
                    alpha + beta + q + 1) * (jacobi_evaluation(q - 1, alpha, beta, 1) ** 2))
    w = np.append(w, wr)
    w = np.append(wl, w)
    x = np.append(x, 1)
    x = np.append(-1, x)
    return [x, w]

def gauss_lobatto_jacobi_quadrature1D(num_points, a, b, alpha=0, beta=0):
    """Gives the 1D Gauss-Lobatto-Jacobi quadrature rule.

    Returns the nodes and weights :math:`\{x_i,w_i\}_{i=1}^Q`, such that:

    .. math::
        \int_a^b f(x) dx \\approx \sum_{i=1}^Q w_i f(x_i)

    The nodes and weights are given by the Gauss-Lobatto-Jacobi quadrature rule.

    Args:
        num_points (int): Number of nodes of the quadrature rule.
        a (float, torch.Tensor): Start of the integration interval.
        b (float, torch.Tensor): End of the integration interval.
        alpha (float, optional): Alpha parameter of the Jacobi polynomial, :math:`> -1`. Defaults to 0.
        beta (float, optional): Beta parameter of the Jacobi polynomial, :math:`> -1`. Defaults to 0.

    Returns:
        roots (torch.Tensor: roots of the Jacobi polynomial.
        weights (torch.Tensor): weights of the quadrature rule.

    Example:

        >>> roots, weights = gauss_lobatto_jacobi_quadrature1D(5, -1, 2)
        >>> print(roots, weights)
        [-1.         -0.48198051  0.5         1.48198051  2.        ] [0.1        0.54444444 0.71111111 0.54444444 0.1       ]

    """

    roots, weights = gauss_lobatto_jacobi_weights(num_points, alpha, beta)
    X = (b - a) / 2 * (roots + 1) + a

    if type(X).__name__ != 'Tensor':
        X = torch.tensor(X)

    return X, torch.tensor(weights)

def gauss_lobatto_jacobi_quadrature2D(num_points_per_dim, a, b, c, d,
                                      alpha_x=0, beta_x=0, alpha_y=0, beta_y=0):
    """Gives the 1D Gauss-Lobatto-Jacobi quadrature rule.

    Returns the nodes and weights :math:`\{x_i, w_i\}_{i=1}^Q`,
    and :math:`\{y_i, \\tilde{w}_i\}_{i=1}^Q`, such that:

    .. math::
        \int_a^b \int_c^d f(x,y) dxdy \\approx \sum_{i,j=1}^Q w_i  \\tilde{w}_j f(x_i, y_j)

    The nodes and weights are given by the Gauss-Lobatto-Jacobi quadrature rule.

    Args:
        num_points_per_dim (int): Number of nodes of the quadrature rule (per dimension).
        a (float): Start of the first integration interval.
        b (float): End of the first integration interval.
        c (float): Start of the second integration interval.
        d (float): End of the second integration interval.
        alpha_x (float, optional): Alpha parameter of the first Jacobi polynomial, :math:`> -1`. Defaults to 0.
        beta_x (float, optional): Beta parameter of the first Jacobi polynomial, :math:`> -1`. Defaults to 0.
        alpha_y (float, optional): Alpha parameter of the second Jacobi polynomial, :math:`> -1`. Defaults to 0.
        beta_y (float, optional): Beta parameter of the second Jacobi polynomial, :math:`> -1`. Defaults to 0.

    Returns:
        X (np.ndarray): roots of the Jacobi polynomial for the first dimension.
        Y (np.ndarray): roots of the Jacobi polynomial for the second dimension.
        Weights_X (np.ndarray): weights of the quadrature rule for the first dimension.
        Weights_y (np.ndarray): weights of the quadrature rule for the second dimension.

    Example:

        >>> X, Y, weights_X, weights_Y = gauss_lobatto_jacobi_quadrature2D(3, -1, 2, 0, 1)
        >>> print(X, "\\n", Y, "\\n", weights_X, "\\n", weights_Y)
        [[-1.   0.5  2. ]
         [-1.   0.5  2. ]
         [-1.   0.5  2. ]]
         [[0.  0.  0. ]
         [0.5 0.5 0.5]
         [1.  1.  1. ]]
         [[0.33333333 1.33333333 0.33333333]
         [0.33333333 1.33333333 0.33333333]
         [0.33333333 1.33333333 0.33333333]]
         [[0.33333333 0.33333333 0.33333333]
         [1.33333333 1.33333333 1.33333333]
         [0.33333333 0.33333333 0.33333333]]

    """

    roots_X, weights_X = gauss_lobatto_jacobi_quadrature1D(
        num_points_per_dim, a, b, alpha_x, beta_x)
    roots_Y, weights_Y = gauss_lobatto_jacobi_quadrature1D(
        num_points_per_dim, c, d, alpha_y, beta_y)

    X, Y = np.meshgrid(roots_X, roots_Y)
    weights_X, weights_Y = np.meshgrid(weights_X, weights_Y)

    return X, Y, weights_X, weights_Y

# INTEGRATION ALGORITHMS
##################################################

def integrate_1d(f, a, b, weights, X=None):
    '''Integrates a function :math:`f(x)` over :math:`[a, b]` using the given roots and weights.

    Note that if X is None, then f must be a np.ndarray or torch.Tensor containing the values
    of the function at the nodes.

    Args:
        f (Iterable or function handle): if f is an array of the same size as weights,
                                         the integral is directly computed using summation.
                                         Otherwise f is evaluated on the roots before hand.
        a (float): start of the interval.
        b (float): end of the interval.
        weights (Iterable): weights of the quadrature.
        X (Iterable, optional): where the function needs to be evaluated (as accorded to the quadrature rule).  Defaults to None.

    Returns:
        integral (float) Value of the integral.

    Example:
        >>> a, b = -1, 2
        >>> roots, weights = gauss_lobatto_jacobi_quadrature1D(5, a, b)
        >>> f = lambda x: x**2
        >>> integral = integrate_1d(f, a, b, weights, roots)
        >>> print(integral)
        3.0

    '''

    if weights is None:
        raise ValueError("No quadrature weights provided (weights = None)")

    if type(weights).__name__ != "ndarray" and type(weights).__name__ != "Tensor":
        weights = torch.Tensor(weights)

    if X is not None and type(X).__name__ != "ndarray" and type(X).__name__ != "Tensor":
        X = torch.Tensor(X)

    if type(f).__name__ == 'function':
        if X is None:
            raise ValueError(
                "f is a function handle but no values were provided for evaluation (roots = None)")
        values = f(X)
    elif type(f).__name__ != "ndarray" and type(f).__name__ != "Tensor":
        values = torch.Tensor(f)
    else:
        values = f

    if type(values).__name__ == 'Tensor':
        integral = (b - a) * torch.sum(values * weights) / 2
    else:
        integral = (b - a) * np.sum(values * weights) / 2

    return integral

def integrate_2d(f, a, b, c, d, weights_X,
                 weights_Y, X=None, Y=None):
    '''
    Integrates a function :math:`f(x,y)` over :math:`[a, b] \\times [c,d]` using the given roots and weights.

    Args:
        f (Iterable or function handle): if f is an array of the same size as weights,
                                         the integral is directly computed using summation.
                                         Otherwise, f is evaluated on the roots beforehand.
        a (float): start of the interval on x.
        b (float): end of the interval on x.
        c (float): start of the interval on y.
        d (float): end of the interval on y.
        weights_X (Iterable): weights for the quadrature.
        weights_Y (Iterable): weights for the quadrature.
        X (Iterable, optional): where the function needs to be evaluated (as accorded to the quadrature rule).  Defaults to None.
        Y (Iterable, optional): where the function needs to be evaluated (as accorded to the quadrature rule). Defaults to None.

    Example:
        >>> a, b, c, d = -1, 2, 0, 1
        >>> X, Y, weights_X, weights_Y = gauss_lobatto_jacobi_quadrature2D(3, a, b, c, d)
        >>> f = lambda x, y: x**2 * y**2
        >>> integral = integrate_2d(f, a, b, c, d, weights_X, weights_Y, X, Y)
        >>> print(integral)
        1.0

    '''

    if weights_X is None or weights_Y is None:
        raise ValueError("No quadrature weights provided (weights = None)")

    # First we convert everything to numpy arrays:
    if X is not None and type(X).__name__ != "ndarray" and type(X).__name__ != "Tensor":
        X = torch.Tensor(X)
    if Y is not None and type(Y).__name__ != "ndarray" and type(Y).__name != "Tensor":
        Y = torch.Tensor(Y)
    if type(weights_X).__name__ != "ndarray" and type(weights_X).__name__ != "Tensor":
        weights_X = torch.Tensor(weights_X)
    if type(weights_Y).__name__ != "ndarray" and type(weights_Y).__name__ != "Tensor":
        weights_Y = torch.Tensor(weights_Y)

    if type(f).__name__ == 'function':
        if X is None or Y is None:
            raise ValueError(
                "f is a function handle but no values were provided for evaluation (roots = None)")
        values = f(X, Y)
    elif type(f).__name__ != "ndarray" and type(f).__name__ != "Tensor":
        values = torch.Tensor(f)
    else:
        values = f

    if type(values).__name__ == 'Tensor':
        integral = (b - a) * (d - c) * \
                   torch.sum(values * weights_X * weights_Y) / 4
    else:
        integral = (b - a) * (d - c) * np.sum(values * weights_X * weights_Y) / 4

    return integral

if __name__ == "__main__":
    import doctest
    doctest.testmod()


#!/usr/bin/env python

import numpy as np

p = np.pi

def DH_to_T(DH):
  """!
  Computes the transformation matrices given the DH table of the serial link.
  @param DH: devavitt-hartemberg parameters.
  @return T: transformation matrices of a joint with respect to previous joint.
  """
  # Get the number of rows, to know how many T matrices should create.
  rows = len(DH)

  T = []

  for i in range(rows):
    Tmp = np.array([[np.cos(DH[i,3]), -np.sin(DH[i,3]), 0, DH[i,1]],
            [np.sin(DH[i,3])*np.cos(DH[i,0]), np.cos(DH[i,3])*np.cos(DH[i,0]), -np.sin(DH[i,0]), -DH[i,2]*np.sin(DH[i,0])],
            [np.sin(DH[i,3])*np.sin(DH[i,0]), np.cos(DH[i,3])*np.sin(DH[i,0]), np.cos(DH[i,0]), DH[i,2]*np.cos(DH[i,0])],
            [0, 0, 0, 1]])
    T.append(Tmp)

  return T

def transformations(T_rel_ini, q, info):
  """!
  Computes tranformations given T_relatives, q's and the info.
  @param T_rel_ini: the ones computed with DH_to_T.
  @param q: current configuration of baxter's arm.
  @param info: 1->revolute, 0->prismatic.
  @return T: transformation matrices of a joint with respect to previous joint in
  the new configuration.
  """
  row_q = q.size
  row_info = info.size

  T = []

  if row_q != row_info:
    print("Warning. q and info must have same size.")
    return

  for i in range(row_q):
    if info[i] == 1:
      Tel = np.array([[np.cos(q[i]), -np.sin(q[i]), 0 , 0],
                      [np.sin(q[i]), np.cos(q[i]), 0 , 0],
                      [0, 0, 1, 0],
                      [0, 0, 0, 1]])
  #  else:
      # Case in which there are prismatic joints.
##      Tel = np.array([[1, 0, 0, 0],
##                      [0, 1, 0, 0],
##                      [0, 0, 0, q[i]]
##                      [0, 0, 0, 1]])

    Tmp = np.dot(T_rel_ini[i], Tel)
    T.append(Tmp)

  # Last matrix is constant in time. T_7,e.e
  T.append(T_rel_ini[row_q])

  return T

def abs_trans(T_rel):
  """!
  Computes trasformations matrices w.r.t. 0 frame.
  @param T_rel: trasformation matrices of a joint with respect to previous one.
  @return T: absolute transformation matrices.
  """
  T = []
  # First is the same.
  T.append(T_rel[0])

  for i in range(1, len(T_rel)):
    Tmp = np.dot(T[i-1], T_rel[i])
    T.append(Tmp)

  return T


import numpy as np

def rms(e):
    ''' Calculate root mean square of a vector of data. '''
    return np.sqrt(np.mean(np.square(e)))

def bound_array(a, lb, ub):
    ''' Elementwise bound array above and below. '''
    return np.minimum(np.maximum(a, lb), ub)

def right_pseudoinverse(J):
    JJT = J.dot(J.T)
    return J.T.dot(np.linalg.inv(JJT))

def rotation_matrix(θ):
    ''' 2D rotation matrix: rotates points counter-clockwise. '''
    return np.array([[np.cos(θ), -np.sin(θ)],
                     [np.sin(θ),  np.cos(θ)]])

def rotation_jacobian(θ):
    ''' Derivative of rotation matrix (above) w.r.t θ. '''
    return np.array([[-np.sin(θ), -np.cos(θ)],
                     [ np.cos(θ), -np.sin(θ)]])

def dist_to_line_segment(p, p1, p2):
    ''' Calculate distance and closest point of p to the line segment with end
        points p1 and p2. '''
    v = (p2 - p1)
    length2 = np.dot(v, v)
    P = (np.eye(2) - np.outer(v, v) / length2)
    a = p1 - p

    # closest point
    pc = np.dot(P, a) + p

    # squared distance between closest point and end points of the line
    d1_2 = np.dot(p1 - pc, p1 - pc)
    d2_2 = np.dot(p2 - pc, p2 - pc)

    # check if closest point falls outside the line segment
    if max(d1_2, d2_2) > length2:
        if d1_2 >= d2_2:
            pc = p2
        else:
            pc = p1

    d2 = np.dot(p - pc, p - pc)
    return pc, d2


# coding: utf-8

# In[33]:

get_ipython().run_cell_magic('javascript', '', '\nIPython.OutputArea.prototype._should_scroll = function(lines) {\n    return false;\n}')

# In[34]:

get_ipython().run_line_magic('config', "InlineBackend.figure_format = 'retina'")

from __future__ import division
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# ## Sample dataset
# We will sample the dataset using normal distribution µ =1 and σ = 0.05.

# y is generated using 1.3x2 + 4.8x + 8 + e

# In[35]:

"""Sample features for the dataset"""
x = np.random.uniform(0.913397, 1.08660, size = (100, 1))

"""Generate target. Here target is given by 1.3x^2 + 4.8x + 8 + e"""
x = sorted(x)
func = np.vectorize(lambda x: (1.3 * (x * x)) + (4.8 * x) + 8)
y = func(x)
e = np.random.rand(100, 1)
y = y + e

# In[36]:

from sklearn.model_selection import train_test_split

xTrain, xTest, yTrain, yTest = train_test_split(x, y,
                                                test_size=0.2)

print("Train set size " +str(len(xTrain)))
print("Test set size " +str(len(xTest)))

# In[37]:

from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import PolynomialFeatures
from sklearn.metrics import mean_squared_error
trainingPipeline = Pipeline([
    ("polynomialFeatures", PolynomialFeatures()),
    ("featureScaling", StandardScaler()),
    ("regression", LinearRegression())
])

pipelineParameters = {"polynomialFeatures__include_bias":False}

# In[38]:

degrees = [1, 2, 7, 10, 16, 100]
fig, axs = plt.subplots(6, 2, figsize=(20, 40))
axs = axs.flatten()
index = 0

mseNoReg = []
fig.suptitle("Actual vs prediction", fontsize = 20)

for degree in degrees:
    pipelineParameters['polynomialFeatures__degree'] = degree
    trainingPipeline.set_params(** pipelineParameters)

    trainingPipeline.fit(x, y)
    yPrediction = trainingPipeline.predict(x)

    #axs[index].plot(yPrediction,y)
    #axs[index].plot(yPrediction, "ro", label = "Prediction")
    #axs[index].plot(y, "gx", label = "Actual")
    #axs[index].legend()

    for i in range(len(yPrediction)):
        axs[index].plot([i,i],[y[i], yPrediction[i]], c = "k", linewidth=0.5)

    axs[index].plot(yPrediction, 'o', label='Prediction', color = 'g')
    axs[index].plot(y,'^' , label='Ground Truth', color = 'r' )
    axs[index].legend(prop={'size': 15})

    axs[index + 1].plot(x, yPrediction, label = "Polynomial curve with degree "+str(degree))
    axs[index + 1].plot(x, y, "go", label = "Ground truth")
    axs[index+1].legend(prop={'size': 15})
    index = index + 2
    mseNoReg.append(mean_squared_error(y, yPrediction))
plt.show()
plt.close()

fig, axs = plt.subplots(1, 1, figsize=(25, 10))

axs.plot(mseNoReg)
index = 0
for data, label in zip(mseNoReg, degrees):
    axs.plot(index, data, "o", label = "Degree-" + str(label))
    index = index + 1
axs.legend(prop={'size': 15})
plt.show()

# As we make model more complex the error is decreasing as it better fits the training set. It is evident from the above graph. However it is not a good practice to train more complex model as it may overfit. To get a best model that performs good on train and test set, regularization should be used. Furthermore we can see as the degree of the polynomial increases the graph becomes complex.

# In[39]:

from sklearn.linear_model import Ridge

trainingPipeline = Pipeline([
    ("polynomialFeatures", PolynomialFeatures()),
    ("featureScaling", StandardScaler()),
    ("regression", Ridge())
])

pipelineParameters = {"polynomialFeatures__include_bias":False}

# In[40]:

degree = 10
regParameters = [0, 1e-10, 1e-8, 1e-6, 1e-4, 1]

fig, axs = plt.subplots(6, 2, figsize=(20, 40))
axs = axs.flatten()
index = 0

mseReg = []
fig.suptitle("Actual vs prediction", fontsize = 20)

for regParameter in regParameters:
    pipelineParameters['polynomialFeatures__degree'] = degree
    pipelineParameters['regression__alpha'] = regParameter

    trainingPipeline.set_params(** pipelineParameters)

    trainingPipeline.fit(x, y)
    yPrediction = trainingPipeline.predict(x)

    for i in range(len(yPrediction)):
        axs[index].plot([i,i],[y[i], yPrediction[i]], c = "k", linewidth=0.5)

    axs[index].plot(yPrediction, 'o', label='Prediction', color = 'g')
    axs[index].plot(y,'^' , label='Ground Truth', color = 'r' )
    axs[index].legend(prop={'size': 15})

    axs[index + 1].plot(x, yPrediction,
                        label = "Ridge regression with regulizarationParameter "+str(regParameter))

    axs[index + 1].plot(x, y, "go",
                        label = "ground truth")

    axs[index + 1].legend(prop={'size': 15})
    index = index + 2
    mseReg.append(mean_squared_error(y, yPrediction))
plt.show()
plt.close()

fig, axs = plt.subplots(1, 1, figsize=(25, 10))

axs.plot(mseReg)
index = 0
for data, label in zip(mseReg, regParameters):
    axs.plot(index, data, "o", label = "Regulizaration Parameter-" + str(label))
    index = index + 1
axs.legend(prop={'size': 15})
plt.show()

# We can see that the model performs poorly as the regularization parameter value is increased. This is expected behaviour. The idea behind regularization is that we want to avoid overfitting on training set so that the model can perform good on test set. Furthermore we can see that the curve simplifies as the regularization parameter value increases.


"""
Given an array S of n integers, are there three distinct elements a, b, c in S such that a + b + c = 0?

Find all unique triplets in the array which gives the sum of zero.

Note: The solution set must not contain duplicate triplets.

For example, given array S = [-1, 0, 1, 2, -1, -4],

A solution set is:
{
  (-1, 0, 1),
  (-1, -1, 2)
}
"""

from typing import List, Tuple, Set
import timeit
import numpy as np

def three_sum_brute_force(array: List[int], target: int) -> Tuple[int, int, int]:
    """
    args:
      array:
      target:

    returns:
      idxs

    >>> s = [-1, 0, 1, 2, -1, -4]
    >>> three_sum_brute_force(s, 0)
    (0, 1, 2)

    O(N^3)
    """

    for i, u in enumerate(array):
        for j, v in enumerate(array):
            for k, w in enumerate(array):
                if j > i and k > j and u + v + w == target:
                    return i, j, k

    return None

def three_sum_using_sort(array: List[int], target: int) -> Tuple[int, int, int]:
    """
    args:
      array:
      target:

    returns:
      idxs

    >>> s = [-1, 0, 1, 2, -1, -4]
    >>> three_sum_hash_map(s, 0)
    (0, 1, 2)

    O(NlogN) + O(N^2) = O(N^2)
    """

    origin_index = list(np.argsort(array))
    sorted_array = [array[i] for i in origin_index]

    for i, u in enumerate(sorted_array[:-2]):
        j, k = i + 1, len(sorted_array) - 1
        while j < k:
            sum_jk = sorted_array[j] + sorted_array[k]
            if sum_jk + u == target:
                return origin_index[i], origin_index[j], origin_index[k]

            if sum_jk + u < target:
                j += 1
            else:
                k -= 1

    return None

from collections import defaultdict

def three_sum_hash_map(array: List[int], target: int) -> Tuple[int, int, int]:
    """
    args:
      array:
      target:

    returns:
      idxs

    >>> s = [-1, 0, 1, 2, -1, -4]
    >>> three_sum_hash_map(s, 0)
    (0, 1, 2)

    O(N^2) + O(N) = O(N^2)
    """
    hash_map_one = defaultdict(list)
    for k, w in enumerate(array):
        hash_map_one[w].append(k)

    for i, u in enumerate(array):
        for j, v in enumerate(array):
            if j > i:
                complement = target - u - v
                for k in hash_map_one[complement]:
                    if k not in [i, j]:
                        return i, j, k

    return None

    # res = set()
    # array.sort()                # O(NlgN)
    # for i in range(len(array) - 2):
    #     if i > 0 and array[i] == array[i - 1]:
    #         continue
    #     l, r = i + 1, len(array) - 1
    #     while l < r:
    #         s = array[i] + array[l] + array[r]
    #         if s > 0:
    #             r -= 1
    #         elif s < 0:
    #             l += 1
    #         else:
    #             # found three sum
    #             res.add((array[i], array[l], array[r]))

    #             # remove duplicates
    #             while l < r and array[l] == array[l + 1]:
    #                 l += 1

    #             while l < r and array[r] == array[r - 1]:
    #                 r -= 1

    #             l += 1
    #             r -= 1
    # return res

def analysis():
    """
    analysis of all implementations
    """
    def wraped(f, *args, **kwargs):
        """
        return a callable object for timeit.timeit
        """
        def wrap():
            return f(*args, **kwargs)
        return wrap

    N = 10000
    array = list(np.random.choice(N * 10, N, replace=False))
    target = 16384
    run_number = 10

    print(three_sum_brute_force(array, target))
    print(three_sum_using_sort(array, target))
    print(three_sum_hash_map(array, target))

    t_brute_force = timeit.timeit(wraped(three_sum_brute_force, array, target), number=run_number)
    t_using_sort = timeit.timeit(wraped(three_sum_using_sort, array, target), number=run_number)
    t = timeit.timeit(wraped(three_sum_hash_map, array, target), number=run_number)

    print("analysis of three_sum")
    print("t(brute_force)={:.3f}\nt(using_sort )={:.3f}\nt(using_hash )={:.3f}".format(t_brute_force, t_using_sort, t))

if __name__ == '__main__':
    # import doctest
    # doctest.testmod()
    analysis()

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        """
        给定一个包含 n 个整数的数组 nums，判断 nums 中是否存在三个元素 a，b，c ，使得 a + b + c = 0 ？找出所有满足条件且不重复的三元组。

        注意：答案中不可以包含重复的三元组。

        例如, 给定数组 nums = [-1, 0, 1, 2, -1, -4]，

        满足要求的三元组集合为：
        [
          [-1, 0, 1],
          [-1, -1, 2]
        ]
        >>> nums = [-1, 0, 1, 2, -1, -4]
        >>> Solution().threeSum(nums)

        """
        result = set()
        sorted_nums = sorted(nums)
        for i, u in enumerate(sorted_nums[:-2]):

            j, k = i + 1, len(sorted_nums) - 1
            while j < k:
                if u + sorted_nums[j] + sorted_nums[k] == 0:
                    result.add((u, sorted_nums[j], sorted_nums[k]))
                    j += 1
                    k -= 1
                elif u + sorted_nums[j] + sorted_nums[k] > 0:
                    k -= 1
                else:
                    j += 1
        return [list(e) for e in result]


import matplotlib.pyplot as plt
import numpy as np
import copy
import time

def brute_force_find_max(array):
    max_profit = 0
    max_left = 0
    max_right = 0

    for i in range(len(array)):
        profit = 0
        for j in range(i,  len(array)-1):
            profit = profit + array[j]
            if profit>max_profit:
                max_left = i
                max_right = j
                max_profit = profit

    return max_left, max_right, max_profit

def find_max_crossing_subarray(array, low, mid, high):
    left_sum = -1500
    sum_subarray = 0
    max_left = 0
    max_right = 0
    for i in range(mid, low-1,-1):
        sum_subarray = sum_subarray + array[i]
        if sum_subarray > left_sum:
            left_sum = sum_subarray
            max_left = i
    right_sum = -1500
    sum_subarray = 0
    for i in range(mid+1, high+1):
        sum_subarray = sum_subarray + array[i]
        if sum_subarray > right_sum:
            right_sum = sum_subarray
            max_right = i
    return max_left, max_right, (left_sum + right_sum)

def find_maximum_subarray(array, low, high):
    if low == high:
        return low, high, 0
    else:
        mid = (low+high)//2
    left_low, left_high, left_sum = find_maximum_subarray(array, low, mid)
    right_low, right_high, right_sum = find_maximum_subarray(array, mid+1, high)
    cross_low, cross_high, cross_sum = find_max_crossing_subarray(array, low, mid, high)
    if left_sum >= right_sum and left_sum >= cross_sum:
        return left_low, left_high, left_sum
    elif right_sum >= left_sum and right_sum >= cross_sum:
        return right_low, right_high, right_sum
    else:
        return cross_low, cross_high, cross_sum

def display_plots( size, brute_time, recursive_time):
    incrementer = brute_time / size
    array_of_time_brute = [incrementer * x for x in range(size)]
    n = np.arange(0, size)
    plt.plot(n, array_of_time_brute,label='$O(n^2)$')

    incrementer = recursive_time / size
    array_of_time_recursive = [incrementer * x for x in range(size)]
    n = np.arange(0, size)
    plt.plot(n, array_of_time_recursive, label='$O(nlgn)$')

    temp = np.argwhere(np.diff(np.sign(np.array(array_of_time_brute) - np.array(array_of_time_recursive)))).flatten()
    print("the intercept is at times: ", temp, " seconds.")

    plt.xlabel('n')
    plt.ylabel('seconds')
    plt.title('Brute Force vs Recursive')
    plt.grid('on')
    plt.legend()
    plt.show()

def main():
    size_of_array = -1

    #makes sure input is positive int
    while size_of_array < 1:
        size_of_array = int(input("Enter the size of the array to be filled: "))
        if size_of_array < 1:
            print("please enter a positive number!")

    # display info to screen
    print("your number was: ", size_of_array)
    random_array_of_ints = np.random.randint(-999, 1001, size_of_array)
    copy_of_array = copy.deepcopy(random_array_of_ints)
    #print("Array: ", random_array_of_ints)

        # here we print sorted array
    #max subarray brute force
    start_brute = time.time()
    max_left, max_right, max_profit = brute_force_find_max(random_array_of_ints)
    end_brute = time.time()
    print("BRUTE FORCE: max left index: ", max_left, "max right index", max_right, "max profit value: ",max_profit)

    random_array_of_ints = copy_of_array

    #max subarray recursive
    start_recursive = time.time()
    max_left, max_right, max_profit = find_maximum_subarray(random_array_of_ints, 0, size_of_array-1)
    end_recursive = time.time()
    print("RECURSIVE: max left index: ", max_left, "max right index", max_right, "max profit value: ", max_profit)

    display_plots(size_of_array, (end_brute-start_brute), (end_recursive-start_recursive))

if __name__ == "__main__":
    main()

Code/Python/CS 460 - Algorithms/A2 - Max Subarray/question1.py


# -*- coding: utf-8 -*-
"""
Created on Sat Feb 20 09:38:50 2021

@author: leyuan
"""

import numpy as np

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

def softmax(x):
    if x.ndim == 2:  #[batch_size, p]
        x = x - np.max(x, axis=1, keepdims=True)
        y = np.exp(x) / np.sum(np.exp(x), axis=1, keepdims=True)
        return y

    x = x - np.max(x)
    return np.exp(x) / np.sum(np.exp(x))

def mean_square_error(y, t):
    return 0.5 * np.sum((y - t)**2)

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    # 将one-hot coding转化成单个coding
    if t.size == y.size:
        t = t.argmax(axis=1)

    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

def numerical_gradient(f, x):
    '''
    NumPy 迭代器对象 numpy.nditer 提供了一种灵活访问一个或者多个数组元素的方式。
    https://blog.csdn.net/m0_37393514/article/details/79563776
    '''
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)

    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        x[idx] = tmp_val + h
        fxh1 = f(x) # f(x+h)

        x[idx] = tmp_val - h
        fxh2 = f(x) # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2*h)

        x[idx] = tmp_val
        it.iternext()

    return grad

# def _numerical_gradient_no_batch(f, x):
#     h = 1e-4  # 0.0001
#     grad = np.zeros_like(x)

#     for idx in range(x.size):
#         tmp_val = x[idx]
#         x[idx] = float(tmp_val) + h
#         fxh1 = f(x)  # f(x+h)

#         x[idx] = tmp_val - h
#         fxh2 = f(x)  # f(x-h)
#         grad[idx] = (fxh1 - fxh2) / (2*h)

#         x[idx] = tmp_val  # 还原值

#     return grad

# def numerical_gradient(f, X):
#     if X.ndim == 1:
#         return _numerical_gradient_no_batch(f, X)
#     else:
#         grad = np.zeros_like(X)

#         for idx, x in enumerate(X):
#             grad[idx] = _numerical_gradient_no_batch(f, x)

#         return grad


import numpy as np

"""
Package for population annealing

Basic structure:
    - an instance is primarily defined by its energy function and a solution region
    - from there, we have different attributes:
        * the cooling schedule
        * the population resampling method
        * the MCMC update rule, including how it generates moves, and how many steps to take for each beta
        * the number of replicas
        * error tolerance etc (stopping criteria)
    - what happens when we want to parallelise all of this?
"""

def energy(i):
    return np.cos(i - 10) - 0.2*i

def Resample(n_replicas,beta_current,beta_next,energies,current_posns):

    Boltzmann_factor = np.zeros((n_replicas,))
    for i in range(n_replicas):

        # Compute the statistical weight of the replica
        Boltzmann_factor[i] = np.exp(-energies[i]*(beta_next - beta_current))

    # Normalise the weights
    total = np.sum(Boltzmann_factor)
    weights = [i/total for i in Boltzmann_factor]

    # Compute the new number of replicas for each solution: here we just use the simplest implementation for now,
    # namely the multinomial distribution
    replicas_resampled = np.random.multinomial(n_replicas, weights)
    # Create the new list of positions
    new_posns = []
    for i in range(len(current_posns)):
        new_posns.extend([current_posns[i]]*replicas_resampled[i])

    return new_posns

def MCMC_Step(x,n_steps,n_replicas,posns,beta_current):

    updated_pos = np.zeros((n_replicas,))
    for i in range(n_steps):
        for j in range(n_replicas):

            # Simple update rule will be to take a random step of length < 1 from current position, left or right
            rand_step = 2*(np.random.rand() - 0.5)
            new_posn = posns[j] + rand_step

            current_energy = energy(posns[j])
            new_energy = energy(new_posn)

            # If the move takes us out of function range, stay where we are
            if new_posn > max(x) or new_posn < min(x):
                updated_pos[j] = posns[j]

            # Otherwise, accept or reject move?
            if new_energy < current_energy:
                updated_pos[j] = new_posn
            else:
                p_accept = np.exp(-beta_current*(new_energy - current_energy))
                coin_flip = np.random.rand()
                if coin_flip < p_accept:
                    updated_pos[j] = new_posn

    return updated_pos

def Run_PopAnnealing(x,betas,MCsteps,replicas):

    """
    other inputs needed:
        energy function,
    """

    # Random intital solutions
    posns = np.random.choice(x,n_replicas)

    for beta in betas:

        # Do resampling

        for step in MCsteps:

            for replica in replicas:

                # Update positions according to Metropolis rule


import math
import numpy

gx_mask = [
    [-1, 0, 1],
    [-2, 0, 2],
    [-1, 0, 1]
]

gy_mask = [
    [1, 2, 1],
    [0, 0, 0],
    [-1, -2, -1]
]

def compute_horizontal_gradient_magnitude(image):
    gradient = [[0] * len(image[0]) for _ in range(len(image))]
    padding = int(len(gx_mask) / 2)

    # iterate over the original image
    for i in range(padding, len(image) - padding):
        for j in range(padding, len(image[i]) - padding):
            # horizontal direction
            xsum = 0
            for k in range(len(gx_mask)):
                for l in range(len(gx_mask[k])):
                    xshift = k - 1
                    yshift = l - 1
                    xsum += image[i + xshift][j + yshift] * gx_mask[k][l]

            # normalize
            gradient[i][j] = numpy.round(xsum / 4)

    return gradient

def compute_vertical_gradient_magnitude(image):
    gradient = [[0] * len(image[0]) for _ in range(len(image))]
    padding = int(len(gy_mask) / 2)

    # iterate over the original image
    for i in range(padding, len(image) - padding):
        for j in range(padding, len(image[i]) - padding):
            # vertical direction
            ysum = 0
            for m in range(len(gy_mask)):
                for n in range(len(gy_mask[m])):
                    xshift = m - 1
                    yshift = n - 1
                    ysum += image[i + xshift][j + yshift] * gy_mask[m][n]

            # normalize
            gradient[i][j] = numpy.round(ysum / 4)

    return gradient

def compute_gradient_magnitude(gx_image, gy_image):
    # sanity check
    assert len(gx_image) == len(gy_image)
    assert len(gx_image[0]) == len(gy_image[0])

    # this will store our gradient magnitude results
    gradient = [[0] * len(gx_image[0]) for _ in range(len(gx_image))]
    padding = int(len(gx_mask)/2)

    # iterate over the original image
    for i in range(padding, len(gx_image) - padding):
        for j in range(padding, len(gx_image[i]) - padding):

            # compute magnitude
            gradient[i][j] = numpy.round(math.sqrt((gx_image[i][j] * gx_image[i][j])
                                                   + (gy_image[i][j] * gy_image[i][j])))

    return gradient

# TODO - compute gx and gy separately
def compute_gradient_angle(gradient, gx, gy):
    # create new array to store result
    angle = [[0] * len(gradient[0]) for _ in range(len(gradient))]

    for i in range(len(gradient)):
        for j in range(len(gradient[i])):
            if gradient[i][j] == 0:
                angle[i][j] = 0
            else:
                # FIXME - check is gx is 0!
                radians = numpy.arctan2(gy[i][j], gx[i][j])
                degrees = numpy.rad2deg(radians)
                if degrees < 0:
                    degrees += 360
                angle[i][j] = degrees

    return angle


# -*- coding: utf-8 -*-
"""
Ejercicio 1: Resolución de un sistema lineal con sustitución progresiva.

Función sust_prog: Calcula utilizando sustitución progresiva la solución
del sistema lineal L*x = b, con L cuadrada triangular inferior no singular.

Argumentos de entrada:
         L: matriz cuadrada triangular inferior no singular del sistema lineal
            L*x = b (array numpy de dos dimensiones).
         b: vector de términos independientes (array numpy de una dimensión).

Argumentos de salida:
         x: solución del sistema L*x = b (array numpy de una dimensión).

Ejemplos:
          L = np.array([[2., 0, 0], [1, 2, 0]])
          b = np.array([2., -1, 0])
          x = sus_prog(L,b)
          print('Solución =', x)
          Salida:
              Error: La matriz L no es cuadrada
              Solución = None

          L = np.array([[2., 0, 0], [1, 2, 0], [1, 1, 0]])
          b = np.array([2., -1, 0])
          x = sus_prog(L,b)
          print('Solución =', x)
          Salida:
              Error: La matriz L es singular
              Solución = None

          L = np.array([[2., 0, 0], [1, 2, 0], [1, 1, 2]])
          b = np.array([2., -1, 0])
          x = sus_prog(L,b)
          print('Solución =', x)
          # Comprobamos que L*x = b
          print('Comprobación: L*x =', np.dot(L,x), 'b = ', b)
          Salida:
              Solución = [ 1. -1.  0.]
              Comprobación: L*x = [ 2. -1.  0.] b =  [ 2. -1.  0.]

"""

import numpy as np

np.set_printoptions(precision = 2)   # sólo dos decimales
np.set_printoptions(suppress = True) # no usar notación exponencial

# Función sus_prog
#-----------------------------------------------------------------------------
def sus_prog(L,b):
    m,n = L.shape   # dimensiones de la matriz L
    # Comprobamos que la matriz L es cuadrada
    if m != n:
        print('La matriz L no es cuadrada')
        return
    # Comprobamos que las dimensiones de L y b son adecuadas
    if n != len(b):
        print('Las dimensiones no son correctas')
        return
    # Comprobamos que la matriz L es no singular
    d = np.diag(L)
    for i in range(0,len(d)):
        if d[i] == 0:
            print("La matriz L es singular")
            return

    # Resolvemos el sistema
    x = np.zeros(len(b))              # definimos el vector solución x
    x[0] = b[0]/L[0,0]   # primera coordenada de x
    for i in range(0,len(b)):
        sumatorio = 0
        for j in range(0,i):
            sumatorio += (L[i,j]*x[j])
        x[i] = (1/L[i,i])*(b[i] - sumatorio)       # restantes coordenadas de x
    return x

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

# Ejemplos de prueba
#--------------------------------------

#PRIMER EJERMPLO DE PRUEBA
L = np.array([[2., 0, 0], [1, 2, 0]])
b = np.array([2., -1, 0])
x = sus_prog(L,b)
print('Solucion =', x)

#SEGUNDO EJEMPLO DE PRUEBA
L = np.array([[2., 0, 0], [1, 2, 0], [1, 1, 0]])
b = np.array([2., -1, 0])
x = sus_prog(L,b)
print('Solucion =', x)

#TERCER EJEMPLO DE PRUEBA
L = np.array([[2., 0, 0], [1, 2, 0], [1, 1, 2]])
b = np.array([2., -1, 0])
x = sus_prog(L,b)
print('Solucion del sistema 1=', x)
# Comprobamos que L*x = b
print('Comprobacion: L*x =', np.dot(L,x), 'b = ', b)

# Ejemplo
#--------------------------------------
n = 5
np.random.seed(1)           # para que los números aleatorios se repitan
L = np.random.random((n,n)) # genera una matriz aleatoria nxn
L = np.tril(L)              # hacer cero los elementos por encima de la diagonal
b = np.random.random(n)  # genera un vector aleatorio de dimensión n
x = sus_prog(L,b)

print('L')
print(L)
print('b')
print(b)
print('Solucion del sistema 2 =', x)


from __future__ import division
# plotting
from matplotlib import pyplot as plt;
if "bmh" in plt.style.available: plt.style.use("bmh");
# scientific
import numpy as np;
import pandas as pd
import scipy as scp;
import scipy.stats;
# Nice Plot of Pandas DataFrame
from IPython.display import display, HTML
# rise config
from notebook.services.config import ConfigManager
cm = ConfigManager()
cm.update('livereveal', {
              'theme': 'simple',
              'start_slideshow_at': 'selected',
})
import warnings
warnings.simplefilter("ignore")

def regression_overfitting_degree(degree0, degree1, degree2, degree3):
    degreelist = np.array([degree0, degree1, degree2, degree3]);
    x = np.linspace(0, 2*np.pi, 13);
    # np.random.randn generates gaussian samples
    y = np.sin(x) + np.random.randn(x.shape[0]) * 0.3;
    xx = np.linspace(0, 2*np.pi, 100);
    plt.figure(figsize=(12,7.5)) ;
    for i in range(0,4):
        degree = degreelist[i]
        coeffs = np.polyfit(x, y, degree);
        poly = np.poly1d(coeffs);
        plt.subplot(2,2,i+1);
        plt.plot(xx, np.sin(xx), "g", linestyle='--'); plt.hold(True)
        plt.plot(x, y, "or");
        plt.plot(xx, poly(xx), color='b', linestyle='-'); plt.hold(False)
        plt.legend(['True Curve','Data','Learned Curve']);
        plt.title(str(degree)+'th Order Polynomial')

def regression_overfitting_datasetsize(size0, size1, size2, size3):
    sizelist = np.array([size0, size1, size2, size3]);
    degree = 12;
    plt.figure(figsize=(12,7.5)) ;
    for i in range(0,4):
        size = sizelist[i]
        x = np.linspace(0, 2*np.pi, size);
        y = np.sin(x) + np.random.randn(x.shape[0]) * 0.3;
        xx = np.linspace(0, 2*np.pi, 100);
        coeffs = np.polyfit(x, y, degree);
        poly = np.poly1d(coeffs);
        plt.subplot(2,2,i+1);
        plt.plot(xx, np.sin(xx), "g", linestyle='--'); plt.hold(True)
        plt.plot(x, y, "or");
        plt.plot(xx, poly(xx), color='b', linestyle='-'); plt.hold(False)
        plt.legend(['True Curve','Data','Learned Curve']);
        plt.title('Training Dataset Size = ' + str(size))

def regression_overfitting_curve():
    #   Plot the training error and test error w.r.t. dataset size
    numItrn = 100
    degreelist = range(0, 14)
    numTrain = 20
    numTest = 50
    dataTrain = np.linspace(0, 2*np.pi, numTrain)
    labelTrain = np.sin(dataTrain) + np.random.randn(dataTrain.shape[0]) * 0.3
    dataTest = np.linspace(0, 2*np.pi, numTest)
    labelTest = np.sin(dataTest) + np.random.randn(dataTest.shape[0]) * 0.3
    errTrain = np.zeros(degreelist.__len__())
    errTest = np.zeros(degreelist.__len__())
    for j in range(0, numItrn):
        for i in range(0, degreelist.__len__()):
            degree = degreelist[i]
            coeffs = np.polyfit(dataTrain, labelTrain, degree)
            poly = np.poly1d(coeffs)
            predTrain = poly(dataTrain)
            predTest = poly(dataTest)
            errTrain[i] += np.sqrt(((predTrain - labelTrain)**2).sum()/numTrain)/numItrn
            errTest[i] += np.sqrt(((predTest - labelTest)**2).sum()/numTest)/numItrn
    plt.figure(figsize=(15,5))
    plt.subplot(1,2,1)
    plt.plot(degreelist, errTrain, color='r', linestyle='-', marker='o'); plt.hold(True)
    plt.plot(degreelist, errTest, color='b', linestyle='-', marker='x'); plt.hold(False)
    plt.legend(['Training Error','Test Error'])
    plt.xlabel('Degree of Linear Regression'); plt.ylabel('RMSE');
    plt.title('Training Error and Test Error v.s. Degree')
    #   Plot the training error and test error w.r.t. degree
    sizelist = 10 * np.array(range(1, 16));
    numTest = 50
    degree = 12
    errTrain = np.zeros(sizelist.shape[0])
    errTest = np.zeros(sizelist.shape[0])
    for j in range(0, numItrn):
        for i in range(0, sizelist.shape[0]):
            numTrain = sizelist[i]
            dataTrain = np.linspace(0, 2*np.pi, numTrain)
            labelTrain = np.sin(dataTrain) + np.random.randn(dataTrain.shape[0]) * 0.3
            dataTest = np.linspace(0, 2*np.pi, numTest)
            labelTest = np.sin(dataTest) + np.random.randn(dataTest.shape[0]) * 0.3
            coeffs = np.polyfit(dataTrain, labelTrain, degree)
            poly = np.poly1d(coeffs)
            predTrain = poly(dataTrain)
            predTest = poly(dataTest)
            errTrain[i] += np.sqrt(((predTrain - labelTrain)**2).sum()/numTrain)/numItrn
            errTest[i] += np.sqrt(((predTest - labelTest)**2).sum()/numTest)/numItrn
    plt.subplot(1,2,2)
    plt.plot(sizelist, errTrain, color='r', linestyle='-', marker='o'); plt.hold(True)
    plt.plot(sizelist, errTest, color='b', linestyle='-', marker='x'); plt.hold(False)
    plt.legend(['Training Error','Test Error'])
    plt.xlabel('Training Dataset Size'); plt.ylabel('RMSE');
    plt.title('Training Error and Test Error v.s. Training Dataset Size')

def regression_overfitting_coeffs():
    degree0 = 0
    degree1 = 3
    degree2 = 9
    degree3 = 12
    degreelist = np.array([degree0, degree1, degree2, degree3])
    x = np.linspace(0, 2*np.pi, degree3+1)
    # np.random.randn generates gaussian samples
    y = np.sin(x) + np.random.randn(x.shape[0]) * 0.3
    y = 100*y
    coeffs = np.zeros([4,degree3+1])
    for i in range(0,4):
        degree = degreelist[i]
        coeffs[i,0:degree+1] = np.polyfit(x, y, degree)
    coeffs[coeffs == 0] = float('nan')
    df = pd.DataFrame(
        coeffs.T,
        columns=["M=0 (Underfitting)", "M=3 (Good)", "M=9 (Overfitting)","M=12 (Overfitting)"],
        index=["w_0","w_1","w_2","w_3","w_4","w_5","w_6","w_7","w_8","w_9","w_10","w_11","w_12"]
    )
    display(df.fillna(''));

def regression_regularization(x, y, xx, degree, lamda):
    Phi_x = np.zeros([x.__len__(), degree+1])
    for i in range(0, degree + 1):
        Phi_x[:, i] = x**i
    coeffs = np.dot(np.dot(np.linalg.inv(np.dot(Phi_x.T,Phi_x) + lamda*np.eye(degree+1)), Phi_x.T), y)
    Phi_xx = np.zeros([xx.__len__(), degree+1])
    for i in range(0, degree + 1):
        Phi_xx[:, i] = xx**i
    prediction_x = np.dot(Phi_x, coeffs)
    prediction_xx = np.dot(Phi_xx, coeffs)
    return prediction_xx, prediction_x, coeffs

def regression_regularization_plot():
    degree = 10
    numData = 13
    x = np.linspace(0, 2*np.pi, numData)
    # np.random.randn generates gaussian samples
    y = np.sin(x) + np.random.randn(x.shape[0]) * 0.4
    xx = np.linspace(0, 2*np.pi, 100)

    plt.figure(figsize=(12,7.5))

    # Plot the Ordinary Least Squares, lambda=0
    plt.subplot(2,2,1)
    prediction_xx, _, _ = regression_regularization(x, y, xx, degree, lamda=0)
    plt.plot(xx, np.sin(xx), "g", linestyle='--'); plt.hold(True)
    plt.plot(x, y, "or")
    plt.plot(xx, prediction_xx, color='b', linestyle='-'); plt.hold(False)
    plt.legend(['True Curve','Data','Learned Curve'])
    plt.title('Ordinary Least Squares (Degree='+str(degree)+')')

    # Plot L2 Regularized Least Squares, lambda=e^1
    plt.subplot(2,2,2)
    prediction_xx, _, _ = regression_regularization(x, y, xx, degree, lamda=np.exp(-1))
    plt.plot(xx, np.sin(xx), "g", linestyle='--'); plt.hold(True)
    plt.plot(x, y, "or")
    plt.plot(xx, prediction_xx, color='b', linestyle='-'); plt.hold(False)
    plt.legend(['True Curve','Data','Learned Curve'])
    plt.title('L2 Regularization (Degree=' + str(degree) + ', $\lambda$=$e^{-1}$' + ')')

    # Plot L2 Regularized Least Squares, lambda=e^40
    plt.subplot(2,2,3)
    prediction_xx, _, _ = regression_regularization(x, y, xx, degree, lamda=np.exp(30))
    plt.plot(xx, np.sin(xx), "g", linestyle='--'); plt.hold(True)
    plt.plot(x, y, "or")
    plt.plot(xx, prediction_xx, color='b', linestyle='-'); plt.hold(False)
    plt.legend(['True Curve','Data','Learned Curve'])
    plt.title('L2 Regularization (Degree=' + str(degree) + ', $\lambda$=$e^{30}$' + ')')

    # Plot the Training Error and Test Error vs. Regularization Coefficient
    plt.subplot(2,2,4)
    lamdalist = np.logspace(-20,30,50, base=np.e)
    numTrain = 15
    numTest = 50
    numItrn = 100
    degree = 10
    errTrain = np.zeros(lamdalist.shape[0])
    errTest = np.zeros(lamdalist.shape[0])
    for j in range(0, numItrn):
        dataTrain = np.linspace(0, 2*np.pi, numTrain)
        labelTrain = np.sin(dataTrain) + np.random.randn(dataTrain.shape[0]) * 0.4
        dataTest = np.linspace(0, 2*np.pi, numTest)
        labelTest = np.sin(dataTest) + np.random.randn(dataTest.shape[0]) * 0.4
        for i in range(0, lamdalist.shape[0]):
            lamda = lamdalist[i]
            predTest, predTrain, _ = regression_regularization(dataTrain, labelTrain, dataTest, degree, lamda)
            errTrain[i] += np.sqrt(((predTrain - labelTrain)**2).sum()/numTrain)/numItrn
            errTest[i] += np.sqrt(((predTest - labelTest)**2).sum()/numTest)/numItrn
    plt.plot(np.log(lamdalist), errTrain, color='r', linestyle='-', marker='o'); plt.hold(True)
    plt.plot(np.log(lamdalist), errTest, color='b', linestyle='-', marker='x'); plt.hold(False)
    plt.legend(['Training Error','Test Error'], loc='lower right')
    plt.xlabel('ln($\lambda$)'); plt.ylabel('RMSE')
    plt.title('Training Error and Test Error v.s. $\lambda$')

def regression_regularization(x, y, xx, degree, lamda):
    Phi_x = np.zeros([x.__len__(), degree+1])
    for i in range(0, degree + 1):
        Phi_x[:, i] = x**i
    coeffs = np.dot(np.dot(np.linalg.inv(np.dot(Phi_x.T,Phi_x) + lamda*np.eye(degree+1)), Phi_x.T), y)
    Phi_xx = np.zeros([xx.__len__(), degree+1])
    for i in range(0, degree + 1):
        Phi_xx[:, i] = xx**i
    prediction_x = np.dot(Phi_x, coeffs)
    prediction_xx = np.dot(Phi_xx, coeffs)
    return prediction_xx, prediction_x, coeffs

def regression_regularization_coeff():
    lamdalist = np.array([0, np.exp(1), np.exp(10)])
    degree = 10
    x = np.linspace(0, 2*np.pi, degree+1)
    xx = np.linspace(0, 2*np.pi, degree+1)
    y = np.sin(x) + np.random.randn(x.shape[0]) * 0.3
    y = 100*y

    coeffs = np.zeros([3,degree+1])
    for i in range(0,lamdalist.__len__()):
        lamda = lamdalist[i]
        _, _, coeffs[i,0:degree+1] = regression_regularization(x, y, xx, degree, lamda)

    coeffs[coeffs == 0] = float('nan')
    df = pd.DataFrame(
        coeffs.T,
        columns=["lambda=0", "lambda=exp^1", "lambda=exp^10"],
        index=["w_0","w_1","w_2","w_3","w_4","w_5","w_6","w_7","w_8","w_9","w_10"]
    )
    display(df.fillna(''));


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""Which alg can you use to solve a system of equations, and least squares.

Ax = b

With A matrix; x and b vectors
"""

import numpy as np

import scipy.linalg as sclin
import scipy

import sys

def pureDiagonal(matrix, debug=False):
    """Matrix is pure diagonal"""

    if (np.ndim(matrix) != 2):
        raise RuntimeError("I'm not prepared for this. </3")

    # '\u2713' = ✓ (unicode tick character)
    if debug:
        print('Analysing matrix: \n', matrix)
        print()

    if isSquared(matrix, debug=True) and \
            np.allclose(matrix, np.diag(np.diag(matrix))):
        print(u'[\u2713] Matrix is pure diagonal.')
        return True
    else:
        print('[X] Matrix is not pure diagonal.')
        return False

def upperTriangular(matrix, debug=False):
    """Matrix is an upper one"""

    if (np.ndim(matrix) != 2):
        raise RuntimeError("I'm not prepared for this. </3")

    # '\u2713' = ✓ (unicode tick character)
    if debug:
        print('Analysing matrix: \n', matrix)
        print()

    if np.allclose(matrix, np.triu(matrix)):
        print(u'[\u2713] Matrix is upper triangular.')
        return True
    else:
        print('[X] Matrix is not upper triangular.')
        return False

def lowerTriangular(matrix, debug=False):
    """Matrix is an upper one"""

    if (np.ndim(matrix) != 2):
        raise RuntimeError("I'm not prepared for this. </3")

    # '\u2713' = ✓ (unicode tick character)
    if debug:
        print('Analysing matrix: \n', matrix)
        print()

    if np.allclose(matrix, np.tril(matrix)):
        print(u'[\u2713] Matrix is lower triangular.')
        return True
    else:
        print('[X] Matrix is not lower triangular.')
        return False

def checkLU(matrix, debug=False):
    if (np.ndim(matrix) != 2):
        raise RuntimeError("I'm not prepared for this. </3")

    # '\u2713' = ✓ (unicode tick character)
    if debug:
        print('Analysing LU for matrix: \n', matrix)
        print()

    if not isSquared(matrix, debug):
        print('[X] Matrix can not be decomposed using LU.')
        return False

    matrixShape = np.shape(matrix)

    try:
        pMatrix, lMatrix, uMatrix = sclin.lu(matrix, permute_l=False,
                                        check_finite=True)
        if (np.allclose(matrix - np.matmul(np.matmul(pMatrix, lMatrix),
                                uMatrix), np.zeros(matrixShape))):
            print(u'[\u2713] Matrix is decomposable using LU.')

            if (np.allclose(pMatrix, np.zeros(matrixShape))):
                print('\tNo P matrix is requiered for decomposing.')

            else:
                print(u'\t[!!!] P matrix is requiered for decomposing.')

            return True
        else:
            print('[X] Matrix is not decomposable using LU.')
            return False
    except:
        print('[X] Matrix is not decomposable using LU. ',
        '(Catched exception: )', sys.exc_info()[0])
        return False

def isSymmetric(matrix, debug=False):

    # '\u2713' = ✓ (unicode tick character)
    if debug:
        print('Analysing symmetry of the matrix: \n', matrix)
        print()

    if isSquared(matrix, debug) and np.allclose(matrix, matrix.T):
        print(u'[\u2713] Matrix is symmetric.')
        return True
    else:
        print('[X] Matrix is not symmetric.')
        return False

def isPositiveDefinite(matrix, debug=False):
    """
    Taken from: https://stackoverflow.com/a/44287862
    """

    # '\u2713' = ✓ (unicode tick character)
    if debug:
        print('Analysing if matrix is positive definite: \n', matrix)
        print()

    # Check if it is symmetric
    if isSymmetric(matrix, debug):
        # Using cholesky to check the check if it's positive deffinite
        try:
            np.linalg.cholesky(matrix)
            print(u'[\u2713] Matrix is positive definite.')
            return True
        except np.linalg.LinAlgError:
            print('[X] Matrix is not positive definite.')
            return False
    else:
        return False

def isSquared(matrix, debug=True):
    matrixShape = np.shape(matrix)
    if (matrixShape[0] != matrixShape[1]):
        if debug:
            print('[X] Matrix is not squared')
        return False
    else:
        if debug:
            print(u'[\u2713] Matrix is squared.')
        return True

def checkCholesky(matrix, debug=False):
    # '\u2713' = ✓ (unicode tick character)
    if debug:
        print('Analysing Cholesky for matrix: \n', matrix)
        print()

    # isPositiveDefinite verifies cholesky :)
    if isPositiveDefinite(matrix, debug=False):
        print(u'[\u2713] Matrix is decomposable using Cholesky.')
        return True
    else:
        print('[X] Matrix is not decomposable using Cholesky.')
        return False

def checkQR(matrix, debug=False):
    # '\u2713' = ✓ (unicode tick character)
    if debug:
        print('Analysing QR for matrix: \n', matrix)
        print()

    try:
        qMatrix, rMatrix = sclin.qr(matrix)
        if np.allclose(matrix, np.matmul(qMatrix, rMatrix)):
            print(u'[\u2713] Matrix is decomposable using QR.')
            return True
        else:
            print('[X] Matrix is not decomposable using QR.')
            return False
    except sclin.LinAlgError:
        print('[X] Matrix is not decomposable using QR.')
        return False

def checkSVD(matrix, debug=False):
    # '\u2713' = ✓ (unicode tick character)
    if debug:
        print('Analysing SVD for matrix: \n', matrix)
        print()

    try:
        uMatrix, sMatrix, vhMatrix = sclin.svd(matrix, full_matrices=True,
                                                check_finite=True)

        # Diagonalize sigma to perform multiplication
        sigma = np.zeros(( np.shape(uMatrix)[1], np.shape(vhMatrix)[0] ))

        for rowEtCol in range(0, len(sMatrix), 1):
            sigma[rowEtCol][rowEtCol] = sMatrix[rowEtCol]

        if debug:
            print('U = \n', uMatrix)
            print('S = \n', sigma)
            print('V = \n', vhMatrix)

        svdProduct = np.matmul(uMatrix, np.matmul(sigma, vhMatrix))

        if debug:
            print('U*U.T = \n', np.matmul(uMatrix, uMatrix.T))
            print('U*S*V = \n', svdProduct)

        if np.allclose(matrix, svdProduct):
            print(u'[\u2713] Matrix is decomposable using SVD.')
            return True
        else:
            print('[X] Matrix is not decomposable using SVD.')
            return False
    except sclin.LinAlgError:
        print('[X] Matrix is not decomposable using SVD.')
        return False

# print(u'[\u2713] Matrix is.')
# print('[X] Matrix is not.')

def solveLinealEq(matrix, debug=False):
    print('Analysing matrix: \n', matrix, '\n')

    if pureDiagonal(matrix, debug):
        print('--->', end=' ')
        print("You're a lucky one. Divide each b_i by a_ii and get your x_i",
            end=' ')
        print('<---')
    elif upperTriangular(matrix, debug):
        print('--->', end=' ')
        print('This system must be solved using backwards substitution.', end=' ')
        print('<---')
    elif lowerTriangular(matrix, debug):
        print('--->', end=' ')
        print('This system must be solved using forward substitution.', end=' ')
        print('<---')
    else:
        if checkCholesky(matrix, debug):
            print('--->', end=' ')
            print('This system is solvable using Cholesky.', end=' ')
            print('<---')
        #elif checkLU(matrix, debug):
        if checkLU(matrix, debug):
            print('--->', end=' ')
            print('This system is solvable using LU decomposition.', end=' ')
            print('<---')
        #else:
        if checkQR(matrix, debug):
            print('--->', end=' ')
            print('This system is solvable using QR decomposition.', end=' ')
            print('<---')

        matrix = np.array([[12, 4], [3, 2], [6, 2]])
        if checkSVD(matrix, debug):
            print('--->', end=' ')
            print('This system is solvable using SVD decomposition.', end=' ')
            print('<---')
    print()

def tests():
    print('='*10, 'Matrix tests', '='*10)

    A = np.eye(5)
    solveLinealEq(A, debug=False)

    A = np.array([[2, 5, 8, 7], [5, 2, 2, 8], [7, 5, 6, 6], [5, 4, 4, 8]])
    solveLinealEq(A, debug=False)

    A = np.array([[12, 4], [3, 2], [6, 2]])
    solveLinealEq(A, debug=False)

if __name__ == "__main__":
    tests()


""" Orbital Mechanics for Engineering Students Problem 1.1
Question:
Given three vectors A, B, and C, prove analytically that:
    (a) A . A = |A|**2
    (b) A . (B x C) = (A x B) . C
    (c) A x (B x C) = B(A . C) - C(A . B)
Written by: J.X.J. Bannwarth
"""
from sympy.vector import CoordSys3D
from sympy.vector import Vector
from sympy import symbols
from sympy import simplify

# Function
def AreEqual(A, B):
    return simplify(A - B).equals(Vector.zero)

# Title
print("Orbital Mechanics for Engineering Students Problem 1.1")

# Coordinate system
N = CoordSys3D('N')
i, j, k = N.base_vectors()

# Define vectors
ax, ay, az = symbols("ax ay az")
bx, by, bz = symbols("bx by bz")
cx, cy, cz = symbols("cx cy cz")
A = ax * i + ay * j + az * k
B = bx * i + by * j + bz * k
C = cx * i + cy * j + cz * k

# (a)
print("(a)")
print("A . A =", A.dot(A))
print("|A|**2 = ", A.magnitude()**2)

# (b)
print("\n(b)")
bLHS = A.dot(B.cross(C))
bRHS = (A.cross(B)).dot(C)

print("A . (B x C) = ", bLHS)
print("(A x B) . C = ", bRHS)
if simplify(bLHS - bRHS) == 0:
    print("A . (B x C) == (A x B) . C")

# (c)
print("\n(c)")
cLHS = simplify(A.cross(B.cross(C)))
cRHS = simplify(B * A.dot(C) - C * A.dot(B))

print("A x (B x C) = ", cLHS)
print("B(A . C) - C(A . B) = ", cRHS)
if AreEqual(cLHS, cRHS):
    print("A x (B x C) == B(A . C) - C(A . B)")


import statistics, scipy.stats as stats, statsmodels.api as sm
import numpy as np, pandas as pd
import matplotlib.pyplot as plt, pylab as py

from statsmodels.tsa.api import SimpleExpSmoothing

from statsmodels.tsa.statespace.exponential_smoothing import ExponentialSmoothing

def sample_gaussian_process(mu, sigma, realisations):
    np.random.seed(1234)
    return np.random.normal(mu, sigma, realisations)

def sample_random_walk(X0, realisations):
    np.random.seed(1234)
    errors = np.random.normal(0, 1, realisations)
    Xt = X0
    for e in errors:
        Xt = Xt + e
        yield Xt

def ses(series, alpha, x0, t):
    """Forecasts elements t+1, t+2, ... of series
    """
    forecasts = np.empty(len(series))
    forecasts[0] =  x0
    for k in range(1,t+2):
        forecasts[k] = alpha*series[k-1] + (1-alpha)*forecasts[k-1]
    for k in range(t+2,len(series)):
        forecasts[k] = forecasts[k-1]
    forecasts[0:t] = np.nan
    return forecasts

def ses_rolling(series, alpha, x0):
    forecasts = np.empty(len(series))
    forecasts[0] =  x0
    for k in range(1,len(series)):
        forecasts[k] = alpha*series[k-1] + (1-alpha)*forecasts[k-1]
    return forecasts

def plot(realisations, forecasts, alpha):
    f = plt.figure(1)
    plt.title("Simple Exponential Smoothing forecasts\n alpha = {}".format(alpha))
    plt.xlabel('Period')
    first, last = next(x for x, val in enumerate(forecasts) if ~np.isnan(val)), len(forecasts)-1
    plt.axvspan(first, last, alpha=0.2, color='blue')
    plt.plot(forecasts, "g", label="Simple Exponential Smoothing forecasts")
    plt.plot(realisations, label="Actual values")
    plt.legend(loc="upper left")
    plt.grid(True)
    f.show()

def residuals(realisations, forecasts):
    return realisations - forecasts

def standardised_residuals(realisations, forecasts):
    residuals = realisations - forecasts
    return (residuals) / statistics.stdev(residuals)

def residuals_plot(residuals):
    f = plt.figure(2)
    plt.xlabel('Period')
    plt.plot(residuals, "g", label="Residuals")
    plt.grid(True)
    f.show()

def residuals_histogram(residuals):
    f = plt.figure(3)
    plt.xlabel('Period')
    num_bins = 30
    plt.hist(residuals, num_bins, facecolor='blue', alpha=0.5, density=True)
    x = np.linspace(-3, 3, 100)
    plt.plot(x, stats.norm.pdf(x, 0, 1))
    f.show()

def residuals_autocorrelation(residuals, window):
    f = plt.figure(4)
    plt.acorr(residuals, maxlags=window)
    f.show()

def simple_exponential_smoothing():
    N, t, alpha, x0 = 200, 160, 0.5, 20
    realisations = pd.Series(sample_gaussian_process(20, 5, N), range(N))
    forecasts = ses(realisations, alpha, x0, t)
    plot(realisations, forecasts, alpha)
    forecasts = ses_rolling(realisations, alpha, x0)
    res = residuals(realisations, forecasts)
    print("E[e_t] = "+str(statistics.mean(res)))
    print("Stdev[e_t] = "+str(statistics.stdev(res)))
    standardised_res = standardised_residuals(realisations, forecasts)
    residuals_plot(res)
    residuals_histogram(standardised_res)
    residuals_autocorrelation(res, None)
    sm.qqplot(standardised_res, line ='45')
    py.show()

def simple_exponential_smoothing_statsmodels():
    N, t, alpha, x0 = 200, 160, 0.5, 20
    realisations = pd.Series(sample_gaussian_process(20, 5, N), range(N))
    mod = SimpleExpSmoothing(realisations[:t+1]).fit(smoothing_level=alpha, initial_level=x0, optimized=False)
    forecasts = mod.forecast(N-(t+1)).rename(r'$\alpha=0.5$')
    plot(realisations, pd.Series(np.nan, range(t+1)).append(forecasts), alpha)
    py.show()

def plot_ci(realisations, forecasts, forecasts_ci, alpha):
    f = plt.figure(1)
    plt.title("Simple Exponential Smoothing forecasts\n State Space Model")
    plt.xlabel('Period')
    first, last = next(x for x, val in enumerate(forecasts) if ~np.isnan(val)), len(forecasts)-1
    plt.axvspan(first, last, alpha=0.2, color='blue')
    plt.plot(forecasts, "g", label="Simple Exponential Smoothing forecasts")
    plt.plot(realisations, label="Actual values")
    t = next(x for x, val in enumerate(forecasts) if ~np.isnan(val)) - 1
    forecast_index = np.arange(t+1, t+1 + len(forecasts_ci))
    plt.fill_between(forecast_index, forecasts_ci.iloc[:, 0], forecasts_ci.iloc[:, 1], color='r', alpha=0.1)
    plt.legend(loc="upper left")
    plt.grid(True)
    f.show()

def simple_exponential_smoothing_ci(stochastic_process):
    N, t, alpha = 200, 160, 0.5
    if stochastic_process == "GP":
        x0 = 20
        realisations = pd.Series(sample_gaussian_process(20, 5, N), range(N))
    elif stochastic_process == "RW":
        x0 = 0
        realisations = pd.Series(list(sample_random_walk(0, N)), range(N))
    else:
        quit()
    mod = ExponentialSmoothing(realisations[:t+1], initialization_method='known', initial_level=x0).fit(disp=False)
    print(mod.summary())
    forecasts = mod.get_forecast(N-(t+1))
    forecasts_ci = forecasts.conf_int(alpha=0.05)
    plot_ci(realisations, pd.Series(np.nan, range(t+1)).append(forecasts.predicted_mean), forecasts_ci, alpha)
    py.show()

# simple_exponential_smoothing()
# simple_exponential_smoothing_statsmodels()
simple_exponential_smoothing_ci("RW")


import numpy as np
import copy
import math
from sympy import Matrix
from ciphers import decodeKCipher, decodeVignere, decodeVignere2, encodeVignere

alphabet = 'abcdefghijklmnopqrstuvwxyzæøå'

def task1():
    print('\nOppgave 1\n', ((232 + 22 * 77 - 18**2) % 8))

def modInverse(a, m) :
    a = a % m;
    for x in range(1, m) :
        if ((a * x) % m == 1) :
            return x
    return 1

def egcd(a, b):
    if a == 0:
        return (b, 0, 1)
    else:
        g, y, x = egcd(b % a, a)
        return (g, x - (b // a) * y, y)

def modinv(a, m):
    g, x, y = egcd(a, m)
    if g != 1:
        raise Exception('modular inverse does not exist')
    else:
        return x % m

def task2():
    mod = 82
    Z = [[str((i*j)%mod).ljust(3,' ') for i in range(1, mod)] for j in range(1, mod)]

    print('\nOppgave 2 A')
    for row in Z:
        print(row)

    print('\nOppgave 2 B')

    invs = []
    for row in Z:
        for num in row:

            inv = modInverse(int(num), mod)
            if inv != 1 and inv not in invs:
                print(inv)
                invs.append(inv)

    print('\nOppgave 2 C')
    print('Rader og kolonner som inneholder 1 er primtall, og alle som inneholder 0 er delelige.')

# har invertibel matrise over Zm dersom determinant er 1
def task3():

    print('\nOppgave 3 A')
    A = np.matrix([[2, -1], [5, 8]])
    mod = 10
    det = A[0, 0] * A[1, 1] - A[0, 1] * A[1, 0]

    if det % mod == 1:
        print(A.I % mod)
    else:
        print('no inverse')

    print('\nOppgave 3 B')
    A = np.matrix([[2, -1], [5, 8]])
    mod = 9
    det = A[0, 0] * A[1, 1] - A[0, 1] * A[1, 0]

    if det % mod == 1:
        print(A.I % mod)
    else:
        print('no inverse')

def task4():

    print('\nOppgave 4 A')
    print('29!,', math.factorial(29))
    print('\nOppgave 4 B')
    print('Kan kombinere med et annet chiffer, som f.eks: blokk chiffer eller skift chiffer')
    print('\nOppgave 4 C')
    print('n! * antall blokker')

def task5():

    encrypted = 'YÆVFB VBVFR ÅVBV'
    k = 12
    print('\nOppgave 5')
    print(decodeKCipher(encrypted, k, alphabet.upper())) # Hjernen er alene

def task6():
    print('\nOppgave 6')

    print('definisjon: x = x1x2 ... -> ek(x1)ek(x2) ...')
    print('nøkler: b * N!')

def task7():

    print('\nOppgave 7 A')

    text = 'Nå er det snart helg'
    key = 'torsk'
    print('encoded:', (cipher := encodeVignere(text, key, alphabet)))
    print('decoded:', decodeVignere2(cipher, key, alphabet))

    print('\nOppgave 7 B')

    text = 'QZQOBVCAFFKSDC'
    key = 'brus'
    print('decoded:', decodeVignere2(text, key, alphabet))

    print('\nOppgave 7 C')

    print('29 ^ 5')

def task8():

    print('\nOppgave 8 A')

    mod = 29
    K = np.matrix([[11, 8],[3, 7]])
    Ki = Matrix(K).inv_mod(mod)
    print(Ki)

    print('\nOppgave 8 B')

    x = [ alphabet.index(c) for c in 'prim']
    x = np.matrix([x[:2], x[2:]])
    y = (x*K) % mod
    print('encoded:', ''.join([alphabet[i] for arr in y.tolist() for i in arr]) )

    print('\nOppgave 8 C')

    y = [ alphabet.index(c) for c in 'toyysn']
    y = np.matrix([ y[:2], y[2:4], y[4:] ])
    x = (y*Ki) % mod
    print('decoded:',''.join([alphabet[a] for b in x.tolist() for a in b]))

    print('\nOppgave 8 D')

    x = [ alphabet.upper().index(c) for c in 'EASY']
    x =  np.matrix([x[:2], x[2:]])
    xi = Matrix(x).inv_mod(29)

    y = [ alphabet.upper().index(c) for c in 'IØÅY']
    y = np.matrix([y[:2], y[2:]])

    k = xi * y % 29
    ki = Matrix(k).inv_mod(29)

    print(k)

    print('encoded:',''.join([alphabet[a] for b in ((x*k) % 29).tolist() for a in b]))
    print('decoded:',''.join([alphabet[a] for b in ((y*ki) % 29).tolist() for a in b]))

def main():

    task1() # fin
    task2() # fin
    task3() # fin
    task4() # fin
    task5() # fin
    task6() # fin
    task7() # fin
    task8() # fin

if __name__ == '__main__':
    main()

TDAT3020 Sikkerhet i programvare og nettverk/ovinger/oving14 - crypto intro/main.py


# -*- coding: utf-8 -*-
"""
Created on Fri Nov 20 15:13:44 2020

@author: Simon Biberger
"""

import numpy as np

def circle(r=1, c=[0,0,0], sampH=360):
    """
    Returns parametrization of a circle with radius 'r' at
    center (cx,cy,cz) and 'sampH' amount of samples.

    The default values are 1, (0,0,0) and 360 for the radius, base point,
    and amount of samples, respectively.
    """
    cx, cy, cz = c
    theta = np.linspace(0, 2*np.pi, sampH)
    x = r* np.cos(theta) - cx
    y = r * np.sin(theta) - cy
    z = cz * np.ones(theta.shape)
    return x, y, z

def filledcircle(r=1, c=[0,0,0], sampH=360, fcirc=20):
    """
    Returns parametrization of a filled circle with radius 'r' at
    center (cx,cy,cz), with 'sampH' amount of horizontal samples,
    i.e. samples of the circle, and 'fcirc' amount of circles that fill the outer circle of radius 'r'.

    The default values are 1, (0,0,0), 360, and 20 for the radius,
    center, horizontal samples, and filling circles, respectively.
    """
    cx, cy, cz = c
    theta = np.linspace(0, 2*np.pi, sampH)
    r = np.linspace(0,r,fcirc)
    theta, r = np.meshgrid(theta,r)

    x= r * np.cos(theta) - cx
    y = r * np.sin(theta) - cy
    z = cz * np.ones(theta.shape)
    return x, y, z

def opencylinder(r=1, h=2, bp=[0,0,0], sampH=360, sampV=50):
    """
    Returns parametrization of an open (i.e. pipelike) cylinder with
    radius 'r', height 'h', base point (bpx,bpy,bpz),
    where 'sampH' and 'sampV' specify the amount of samples used
    horizontally, i.e. for circles, and vertically, i.e. for height.
    The base point is in the cylinder's center at the bottom.

    The default values for the radius, height, center, 'sampH' and 'sampV'
    are 1, 2, (0,0,0), 360 and 50, respectively.
    """
    bpx, bpy, bpz = bp
    theta = np.linspace(0, 2*np.pi, sampH)
    z = np.linspace(bpz, bpz+h, sampV)
    theta, z = np.meshgrid(theta, z)
    x = r * np.cos(theta) - bpx
    y = r * np.sin(theta)- bpy
    return x, y, z

def opencylinder2(r=1, h=2, bp=[0,0,0], sampH=0.1, sampV=0.1):
    """
    Returns parametrization of an open (i.e. pipelike) cylinder with
    radius 'r', height 'h', base point (bpx,bpy,bpz),
    where 'sampH' and 'sampV' specify the amount of samples used
    horizontally, i.e. for circles, and vertically, i.e. for height.
    The base point is in the cylinder's center at the bottom.

    The default values for the radius, height, center, 'sampH' and 'sampV'
    are 1, 2, (0,0,0), 360 and 50, respectively.

    Difference to opencylinder() is the use of np.arange instead
    of np.linspace in the initialization of theta.
    """
    bpx, bpy, bpz = bp
    theta = np.arange(0, 2*np.pi, sampH)
    z = np.arange(bpz, bpz+h, sampV)
    theta, z = np.meshgrid(theta, z)
    x = r * np.cos(theta) - bpx
    y = r * np.sin(theta)- bpy
    return x, y, z

def closedcylinder(r=1, h=2, bp=[0,0,0], sampH=360, sampV=50, fcirc=20):
    """
    Returns the parametrization of a closed cylinder with radius 'r',
    height 'h', base point (bpx,bpy,bpz), where 'sampH', 'sampV' specify
    the amount of samples used horizontally, i.e. for circles,
    vertically, i.e. for height, and 'fcirc' specifies the amount
    of circles that fill the outer perimeter of radius 'r' of the cylinder,
    i.e. how many circles are used to fill the bottom and top.
    The base point is in the cylinder's center at the bottom.

    The default values for the radius, height, center, 'sampH', 'sampV',
    'fcirc' are 1, 2, (0,0,0), 360, 50, and 20, respectively.
    """
    bpx, bpy, bpz = bp
    theta = np.linspace(0, 2*np.pi, sampH)
    z = np.linspace(bpz, bpz+h, sampV )
    theta, z = np.meshgrid(theta, z)
    x = r * np.cos(theta) - bpx
    y = r * np.sin(theta) - bpy

    xcirc, ycirc, zcirc = filledcircle(r=r,c=[bpx,bpy,bpz], sampH=sampH,
                                       fcirc=fcirc)

    x = np.append(x,xcirc,0)
    y = np.append(y,ycirc,0)
    z = np.append(z,zcirc,0)

    x = np.append(x,xcirc,0)
    y = np.append(y,ycirc,0)
    z = np.append(z,zcirc+h,0)

    return x, y, z

def sphere(r=1, c=[0,0,0], samp=360):
    """
    Returns parametrization of a sphere with radius 'r' at
    center (cx,cy,cz) and 'samp' amount of samples in all directions.

    The default values are 1, (0,0,0) and 360 for the radius, center,
    and amount of samples, respectively.
    """
    cx, cy, cz = c
    theta = np.linspace(0, 2*np.pi, samp)
    phi = np.linspace(0, np.pi, samp)

    phi, theta = np.meshgrid(phi, theta)
    #z = cz*np.ones(theta.shape)

    r_xy = r*np.sin(phi)
    x = cx + r_xy * np.cos(theta)
    y = cy + r_xy * np.sin(theta)
    z = cz + r * np.cos(phi)

    return x, y, z

def opencone(r=1, h=5, bp=[0,0,0], sampH=360, sampV=50):
    """
    Returns parametrization of an open cone with radius 'r' and height 'h at
    basepoint (bpx,bpy,bpz), where 'sampH' and 'sampV' specify the amount of
    samples used horizontally, i.e. for circles, and vertically, i.e.
    for height. The base point is in the cones's center at the bottom.

    The default values are 1, 5, (0,0,0), 360 and 50 for the radius, center,
    and amount of horizontal and vertical samples, respectively.
    """
    bpx, bpy, bpz = bp
    theta0 = np.linspace(0, 2*np.pi, sampH)
    z = np.linspace(bpz, bpz+h, sampV)
    theta, z = np.meshgrid(theta0, z)
    r = np.linspace(r, 0, sampV)
    theta, r = np.meshgrid(theta0, r)

    x = r * np.cos(theta) - bpx
    y = r * np.sin(theta) - bpy

    return x, y, z

def closedcone(r=1, h=5, bp=[0,0,0], sampH=360, sampV=50, fcirc=20):
    """
    Returns parametrization of a closed cone with radius 'r' and height 'h at
    basepoint (bpx,bpy,bpz), where 'sampH' and 'sampV' specify the amount of
    samples used horizontally, i.e. for circles, and vertically, i.e.
    for height, and 'fcirc' specifies the amount
    of circles that fill the bottom of the cone with radius 'r',
    The base point is in the cones's center at the bottom.

    The default values are 1, 5, (0,0,0), 360 and 50 for the radius, center,
    and amount of horizontal and vertical samples, respectively.
    """
    bpx, bpy, bpz = bp
    theta0 = np.linspace(0, 2*np.pi, sampH)
    z = np.linspace(bpz, bpz+h, sampV)
    theta, z = np.meshgrid(theta0, z)
    r = np.linspace(r, 0, sampV)
    theta, r = np.meshgrid(theta0, r)

    x = r * np.cos(theta) - bpx
    y = r * np.sin(theta) - bpy

    xcirc, ycirc, zcirc = filledcircle(r=r,c=[bpx,bpy,bpz], sampH=sampH,
                                       fcirc=fcirc)

    x = np.append(x,xcirc,0)
    y = np.append(y,ycirc,0)
    z = np.append(z,zcirc,0)

    return x, y, z

def testshape_ocg():
    '''
    Returns the occupancy grid of a testshape used in some parts of the
    thesis.
    '''
    W=201
    H=20
    total=np.zeros((W,W,W), np.int8)
    for i in range(H):
        total[i,0:W,0:W]=1
    for i in range(W//2):
        total[W//2+H-1-i,0:W,W//2-i:W//2+i+1]=1
    radius = 75
    R = 2*radius +1
    center=(radius, radius, radius)
    a=[]
    b=[]
    c=[]
    diff=W-R
    for z in range(R):
        for y in range(R):
            for x in range(R):
                val=(x-center[2])**2+(y-center[1])**2+(z-center[0])**2
                if val<=radius**2:
                    a.append(x)
                    b.append(y)
                    c.append(z)
                    total[diff//2+z+H-1,diff//2+y,diff//2+x]=1
    return total


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sat Jul 10 09:22:35 2021

@author: alessandro
"""

import math
import numpy as np
import matplotlib.pyplot as plt
import sympy as sym
from sympy.utilities.lambdify import lambdify
from scipy.optimize import fsolve

# Dati
x = sym.symbols('x')
fx = x - 2 * sym.sqrt(x - 1)
dfx = sym.diff(fx, x, 1)
f = lambdify(x, fx, np)
df = lambdify(x, dfx, np)
a = 1
b = 3
zero = 2

xx = np.linspace(a, b, 100)
yy = f(xx)
dyy = df(xx)
plt.ylim((-1, 1))
plt.plot(xx, 0*xx)
plt.plot(zero, f(zero), 'o')
plt.plot(zero, df(zero), 'o')
plt.plot(xx, yy)
plt.plot(xx, dyy)
plt.show()

# Serve newton di molteplicità 2 per ottenere una convergenza quadratica
def newton(f, df, m, x0, tolx, tolf, nmax=2048):
    def delta(x): return f(x) / df(x) if abs(df(x)) > np.spacing(1) else print("derivata nulla")
    def prossimax(x): return x - m * delta(x)

    x = prossimax(x0)
    fx = f(x)
    it, xk = 1, [x]
    while it < nmax and abs(fx) > tolf and abs(delta(x)) > tolx * abs(x):
        x = prossimax(x)
        xk.append(x)
        fx = f(x)
        it += 1
    return x, it, xk

zero, it, xk = newton(f, df, 2, 3, 1e-12, 1e-12)
print(f"lo zero è in {zero}, con {it} iterate")

def stima(xk):
    k = len(xk) - 4
    n = np.log(np.abs(xk[k + 3] - xk[k + 2]) / np.abs(xk[k + 2] - xk[k + 1]))
    d = np.log(np.abs(xk[k + 2] - xk[k + 1]) / np.abs(xk[k + 1] - xk[k]))
    return n / d

ordine = stima(xk)
print(f"Ordine di convergenza stimato = {ordine}")

plt.title("Iterate metodo Newton")
plt.semilogy(range(1, it + 1), xk)

zero = newton(f, df, 2, 1, 1e-12, 1e-12)
print(f"ZERO = {zero}")

import numpy,from scipy,import sympy,from sympy


# -*- coding: utf-8 -*-
"""
Created on Tue Dec 29 20:07:41 2015

@author: ACHARI BERRADA YOUSSEF && BARAA ELALAMI, X2013
"""

import numpy as np #import de numpy sous l'alias np
import numpy.random as npr #import de numpy.random sous l'alias npr
import numpy.linalg as npl # for the computation of the eigenvalues
import matplotlib.pyplot as plt #import de matplotlib.pyplot sous l'alias plt
from numpy import sqrt
import pylab as plb # Pour Histogramme
from pylab import pi

"""
----------------------------------Partie I-------------------------------------
Trajectoires de N(petit) valeurs propres associees au mouvement brownien de
                                    Dyson.
"""
#Initialisation parametre de la Partie I
N = 40
T = 1
n = 2**10

"""
Etape 1 : Construction d'un vecteur de k mouvement brownien de variance sigma^2
        discretise au pas de h = T/n entre [0,T]
"""
def bM(k,T,n,sigma) :
    h= T/n
    W = sqrt(h)* sigma * npr.randn(k,n+1)
    W[:,0]=np.zeros(k)
    W = np.cumsum(W, axis=1)
    return W

#W = bM(2,1,n,1/sqrt(N/2))
"""
Etape 2 : Construction de la matrice aleatoire de taille N A_t au temps
            t = k * T/n avec k=0..n
"""

def aleaMatrix(N,n,T) :

    A = np.zeros([n+1,N,N])
    #remplissage de la diagonale
    sigmaDiag = 1/sqrt(N/2)
    Wii = bM(N,T,n,sigmaDiag)
    for t in range(n+1):
        np.fill_diagonal(A[t],Wii[:,t])
    #remplissage symetrique du reste de la matrice
    sigmaHorsDiag = 1/sqrt(N)
    for i in range(0,N-1) :
        Wij = bM(N-1-i,T,n,sigmaHorsDiag)
        for t in range(n+1):
            A[t,i,i+1:N] = Wij[:,t]
            A[t,i+1:N,i] = Wij[:,t]

    return A

#A=aleaMatrix(N,n,T)

"""
Etape 3 : Calcule des valeurs propres de A_t pour tout t = k * T/n avec k=0..n
"""

def valeurPropre(N,n,T) :
    Lambda = np.zeros([N,n+1])
    A = aleaMatrix(N,n,T)
    for t in range(n+1) :
        Lambda[:,t]= npl.eigvalsh(A[t])

    return Lambda

#Lambda = valeurPropre(N,n,T)

"""
Etape 4 : Tracer les Trajectoires des valeurs propres
"""
def plotTraj(N,n,T) :
    Tx = np.linspace(0,T,n+1)
    Lambda = valeurPropre(N,n,T)
    for n in range(N):
        plt.plot(Tx,Lambda[n,:])
    plt.title('Trajectoires des $N = %s$ valeurs propres'%(N))
    plt.show()

plotTraj(N,n,T)

"""
----------------------------------Partie II------------------------------------
Histogramme des valeurs propres au temps T pour un N assez Grand.
"""
#Initialisation parametre de la Partie II
N=2000
T=1
"""
Etape 1 : Construction de la matrice aleatoire au temps T
"""
def aleaMatrixFin(N,T) :

    A = np.zeros([N,N])
    #remplissage de la diagonale
    sigmaDiag = 1/sqrt(N/2)
    Wii = sigmaDiag * sqrt(T)* npr.randn(N)
    np.fill_diagonal(A,Wii)

    #remplissage symetrique du reste de la matrice
    sigmaHorsDiag = 1/sqrt(N)
    for i in range(0,N-1) :
        Wij = sigmaHorsDiag * sqrt(T) * npr.randn(N-1-i)
        for t in range(n+1):
            A[i,i+1:N] = Wij
            A[i+1:N,i] = Wij

    return A

#A = aleaMatrixFin(10,1)

"""
Etape 2 : Histogramme des valeurs propres au temps T et loi de Wigner
"""

def histLambdaT(N,T) :

    A = aleaMatrixFin(N,T)
    Lambda= npl.eigvalsh(A)

    plt.title('Histogramme de $N = %s$ valeurs propres' %(N))
    plb.hist(Lambda, normed="True", bins=2*int(sqrt(N)),label=u"Histogramme des valeurs propres")
    x=np.linspace(-2.,2.,100)# discretisation de l'intervalle [-2,2]
    f_x= np.sqrt(4.-x**2)/(pi*2)# discretisation de la densite theorique de Wigner
    plt.plot(x, f_x, "r", label=u"Densite de la loi de Wigner")
    plt.legend(loc='lower right')
    plt.show()

histLambdaT(N,T)


import numpy as np

"""
Editing distance and longest common susequence (lcs) problem
The problems are very similar and can be derived from one another
with minor changes to the source code
"""

def edit_distance(seq1, seq2):
    """
    Compute the solutions of all subproblems:  bottom-up
    Uses dynamic programming. Takes polynomial time
    O(n * m) complexity (time and space)

    input: numpy.array with chars
    """
    # find the length of the strings
    n = seq1.shape[0]
    m = seq2.shape[0]

    # base case, empty string
    if n == 0 or m == 0:
        return np.max(n, m)

    #init lookup table
    ed = np.zeros((n + 1, m + 1), dtype=int)
    ed[:, 0] = np.arange(n + 1)
    ed[0, :] = np.arange (m + 1)

    for i in range(1, n + 1):
        for j in range(1, m + 1):
            # values are diff. by default (just because, removes an else statement)
            dist = 1
            if seq1[i - 1] == seq2[j - 1]:
                dist = 0
            ed[i, j] = np.min([ed[i - 1, j - 1] + dist, ed[i - 1, j] + 1, ed[i, j - 1] + 1])

    return ed[n, m]

def lcs(seq1, seq2):
    """
    Longest Common Susequence (not to be confused with subtring)
    Compute the solutions of all subproblems:  bottom-up
    Uses dynamic programming. Takes polynomial time
    Subsequences do not need to ocupy contiguous positions
    O(n * m) complexity (time and space)
    """
    # find the length of the strings
    n = seq1.shape[0]
    m = seq2.shape[0]

    # base case, empty string
    if n == 0 or m == 0:
        return 0

    #init lookup table
    ed = np.zeros((n + 1, m + 1), dtype=int)
    #ed[:, 0] = np.arange(n + 1)
    #ed[0, :] = np.arange (m + 1)

    for i in range(1, n + 1):
        for j in range(1, m + 1):
            # matching chars, increment seq. length
            if seq1[i - 1] == seq2[j - 1]:
                ed[i, j] = ed[i - 1, j - 1] + 1
            # end of sub-sequence
            else:
                ed[i, j] = np.max([ed[i - 1, j], ed[i, j - 1]])

    return ed[n, m]

def longest_common_substring(s1, s2):
    """
    Get the longest common substring in s1 and s2
    """
    n = s1.shape[0]
    m = s2.shape[0]

    # init. aux. table
    ed = np.zeros((n + 1, m + 1))

    # length of longest common substring
    max_sub_str_len = 0
    # index of the last char of longest common substring in s1
    end_sub_str_i = 0
    # index of the last char of longest common substring in s1
    end_sub_str_j = 0

    for i in range(1, 1 + n):
        for j in range(1, 1 + m):
            # matched chars
            if s1[i - 1] == s2[j - 1]:
                ed[i][j] = ed[i - 1][j - 1] + 1
                # update longest_common_substring
                if ed[i][j] > max_sub_str_len:
                    # update max length
                    max_sub_str_len = ed[i][j]
                    # update end of subtring index
                    end_sub_str_i = i
                    end_sub_str_j = j
            # unmatched chars
            else:
                ed[i][j] = 0

    #lcsstr = s1[int(end_sub_str_i - max_sub_str_len): end_sub_str_i]

    return [int(end_sub_str_i - max_sub_str_len), end_sub_str_i]


from __future__ import division
import warnings

import numpy as np

def interval(x, f):
    """
    Find the shorest interval between two samples in `x`, which contains `f`
    fraction of samples.

    :param x: [float]
        samples for which to compute the interval
    :param f: float
        fraction of samples to include in the interval

    :return: x1, x2, i1, i2
        x1: lower interval value of x
        x2: upper interval value of x
        i1: sorted index of lower interval value
        i2: sorted index of lupper interval value
    """
    if f <= 0 or f > 1:
        raise ValueError("f must be bound by [0, 1)")

    x = np.asarray(x)
    isort = np.argsort(x)
    xsort = x[isort]

    # Number of samples inside the interval. Don't alow less than 1
    nin = max(1, int(f*len(x)))
    # Warn if the interval contains only one sample, as this is meaningless
    if nin == 1:
        warnings.warn(
            "Insufficient samples for proper interval",
            RuntimeWarning)
    # Number of samples outside the interval
    nout = len(x) - nin

    if nout <= 0:
        # Interval includes all points
        i1 = 0
        i2 = len(x)
    else:
        # Possible left interval edges are 0..nout+1 (i.e. interval contains
        # all left points ... excludes up to nout points on the left). Likewise
        # for the right interval edges
        intervals = xsort[-(nout+1):] - xsort[:nout+1]
        # Get the index of the shortest interval
        i1 = np.argmin(intervals)
        i2 = i1 + nin

    # Values inside the interval, at the edges
    x1 = xsort[i1]
    x2 = xsort[i2-1]

    return x1, x2, i1, i2

def half_sample_mode(x):
    """
    Compute the half sample mode of the data samples `x`.

    Algorithm iteratively finds the region of highest density, halving the
    samples to search each time. The algorithm is linear time in x, though
    it first sorts.

    :param x: [float]
        samples for which to compute the half sample mode
    :return: float
        estimated mode
    """
    x = np.asarray(x)
    isort = np.argsort(x)
    xsort = x[isort]

    # The points being considered in current interval
    xpart = xsort

    # Shorten the interval iteratively until at most two points are left
    while len(xpart) > 2:
        # Don't allow interval to contain less than 2 samples; i.e. if second
        # last interval is of 3 samples, next interval will contain 2.
        nin = max(2, int(0.5*len(xpart)))
        nout = len(xpart) - nin
        intervals = xpart[-(nout+1):] - xpart[:nout+1]
        i1 = np.argmin(intervals)
        i2 = i1 + nin
        xpart = xpart[i1:i2]

    mode = np.mean(xpart)

    return mode


# Standard Python
import copy

# Thirdparty
import numpy as np

class Activation():
    '''
    Base class for an activation layer
    Inspired by
    https://github.com/eriklindernoren/ML-From-Scratch/blob/master/mlfromscratch/deep_learning/activation_functions.py
    '''

    def __call__(self, x):
        return

    def derivative(self, x):
        return

class Relu(Activation):

    def __call__(self, x):
        return np.absolute(x * (x > 0))

    def derivative(self, x):
        return np.absolute(1. * (x > 0))

class LeakyRelu(Activation):

    def __init__(self, mu=0.05):
        self.mu = mu
        return

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

    def derivative(self, x):
        x[x>=0] = 1
        x[x<1] = self.mu
        return x

class Linear(Activation):

    def __call__(self, x):
        return x

    def derivative(self, x):
        return 1

class Sigmoid(Activation):

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

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

class Softmax(Activation):

    def __call__(self, x):
        # Using normalised x for numerical stability
        norm_x = x - np.max(x, axis=0)
        return np.exp(norm_x) / np.exp(norm_x).sum(axis=0, keepdims=True)

    def derivative(self, x):
        batch_jacobian = np.apply_along_axis(
            lambda col: np.diag(col) - np.outer(col, col), 0, self.__call__(x))
        return batch_jacobian


import matplotlib
matplotlib.use("TKagg")
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns; sns.set()

def show():
    plt.show()
    plt.clf()

# Generate some data
from sklearn.datasets.samples_generator import make_blobs
X, y_true = make_blobs(n_samples=400, centers=4,
                       cluster_std=0.60, random_state=0)
X = X[:, ::-1] # flip axes for better plotting

# Plot the data with K Means Labels
from sklearn.cluster import KMeans
kmeans = KMeans(4, random_state=0)
labels = kmeans.fit(X).predict(X)
plt.scatter(X[:, 0], X[:, 1], c=labels, s=40, cmap='viridis');
show()

from scipy.spatial.distance import cdist

def plot_kmeans(kmeans, X, n_clusters=4, rseed=0, ax=None):
    """Visualize k-means cluster hypersphere"""
    labels = kmeans.fit_predict(X)

    # plot the input data
    ax = ax or plt.gca()
    ax.axis('equal')
    ax.scatter(X[:, 0], X[:, 1], c=labels, s=40, cmap='viridis', zorder=2)

    # plot the representation of the KMeans model
    centers = kmeans.cluster_centers_
    radii = [cdist(X[labels == i], [center]).max()
             for i, center in enumerate(centers)]
    for c, r in zip(centers, radii):
        ax.add_patch(plt.Circle(c, r, fc='#CCCCCC', lw=3, alpha=0.5, zorder=1))

kmeans = KMeans(n_clusters=4, random_state=0)
plot_kmeans(kmeans, X)
show()

#kmeans always circular
rng = np.random.RandomState(13)
X_stretched = np.dot(X, rng.randn(2, 2))

kmeans = KMeans(n_clusters=4, random_state=0)
plot_kmeans(kmeans, X_stretched)
show()

#Generalizing E-M: Gaussian Miture Models

from sklearn.mixture import GaussianMixture as GMM
gmm = GMM(n_components=4).fit(X)
labels = gmm.predict(X)
plt.scatter(X[:, 0], X[:,1], c=labels, s=40, cmap='viridis')
show()

#because this is probablisitc we also have a probablility matrix
#of size [n_sample, n_clusters]
probs = gmm.predict_proba(X)
print(probs[:5].round(3))

#we can use this data to vary the size in our cluster plot
size = 50 * probs.max(1) ** 2  # square emphasizes differences
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis', s=size);
show() #bigger is better

#some helper plotting functions
from matplotlib.patches import Ellipse

def draw_ellipse(position, covariance, ax=None, **kwargs):
    """Draw an ellipse with a given position and covariance"""
    ax = ax or plt.gca()

    # Convert covariance to principal axes
    if covariance.shape == (2, 2):
        U, s, Vt = np.linalg.svd(covariance)
        angle = np.degrees(np.arctan2(U[1, 0], U[0, 0]))
        width, height = 2 * np.sqrt(s)
    else:
        angle = 0
        width, height = 2 * np.sqrt(covariance)

    # Draw the Ellipse
    for nsig in range(1, 4):
        ax.add_patch(Ellipse(position, nsig * width, nsig * height,
                             angle, **kwargs))

def plot_gmm(gmm, X, label=True, ax=None):
    ax = ax or plt.gca()
    labels = gmm.fit(X).predict(X)
    if label:
        ax.scatter(X[:, 0], X[:, 1], c=labels, s=40, cmap='viridis', zorder=2)
    else:
        ax.scatter(X[:, 0], X[:, 1], s=40, zorder=2)
    ax.axis('equal')

    w_factor = 0.2 / gmm.weights_.max()
    for pos, covar, w in zip(gmm.means_, gmm.covariances_, gmm.weights_):
        draw_ellipse(pos, covar, alpha=w * w_factor)

gmm = GMM(n_components=4, random_state=42)
plot_gmm(gmm, X)
show()

#gmm can also fit oblong data
gmm = GMM(n_components=4, covariance_type='full', random_state=42)
plot_gmm(gmm, X_stretched)
show()

#This then adresses the two main issues with kmeans

#GMM as Density Estimation

#make some data
from sklearn.datasets import make_moons
Xmoon, ymoon = make_moons(200, noise=.05, random_state=0)
plt.scatter(Xmoon[:, 0], Xmoon[:, 1]);

gmm2 = GMM(n_components=2, covariance_type='full', random_state=0)
plot_gmm(gmm2, Xmoon) #not that usefull
show()

#we can ignore clustering lables and use more compenents for a better fit
gmm16 = GMM(n_components=16, covariance_type='full', random_state=0)
plot_gmm(gmm16, Xmoon, label=False)
show()

#we can use this to model the distrobution

##How many components?

n_components = np.arange(1, 21)
models = [GMM(n, covariance_type='full', random_state=0).fit(Xmoon)
          for n in n_components]

plt.plot(n_components, [m.bic(Xmoon) for m in models], label='BIC')
plt.plot(n_components, [m.aic(Xmoon) for m in models], label='AIC')
plt.legend(loc='best')
plt.xlabel('n_components');
show()
#optimal is the minimization
#this is how well GMM works as a density estimator

#example GMM for generating new data

#lets get the digit data
from sklearn.datasets import load_digits
digits = load_digits()

def plot_digits(data):
    fig, ax = plt.subplots(10, 10, figsize=(8, 8),
                           subplot_kw=dict(xticks=[], yticks=[]))
    fig.subplots_adjust(hspace=0.05, wspace=0.05)
    for i, axi in enumerate(ax.flat):
        im = axi.imshow(data[i].reshape(8, 8), cmap='binary')
        im.set_clim(0, 16)

#lets plot the first 100
plot_digits(digits.data)
show()
#This is a little high dimensionality for gmm
#we can filter with a pca

from sklearn.decomposition import PCA
pca = PCA(0.99, whiten=True)
data = pca.fit_transform(digits.data)
#about 1/3rd reduction

#now lets use AIC to see how many components we need
n_components = np.arange(50, 210, 10)
models = [GMM(n, covariance_type='full', random_state=0)
          for n in n_components]
aics = [model.fit(data).aic(data) for model in models]
plt.plot(n_components, aics)
show()
#140 components will do

gmm = GMM(140, covariance_type='full', random_state=0)
gmm.fit(data)
print(gmm.converged_) #check

#now we can draw some new numbers
data_new, labels = gmm.sample(n_samples=100)
print(data_new.shape)

#and we can rebuild them with inverse pca
digits_new = pca.inverse_transform(data_new)
plot_digits(digits_new)
show()


"""
Purpose of this file is to give examples of slicing

This script is partially dirived from the LinkedIn learning course
https://www.linkedin.com/learning/numpy-data-science-essential-training/create-arrays-from-python-structures

"""
import numpy as np

# numpy array are zero index ref
my_vector = np.array([-17, -4, 0, 2, 21, 37, 105])

# we can print the first element of the array as such
print(f'Print the first element of the array {my_vector[0]}')

# numpy array are mutable, that we can change an element
my_vector[0] = -10
print(f'numpy arrays are mutable, the first elemnt changed : {my_vector}')

# we can select any index from -[size] to [size -1], any value outside this will
# generate an error

print(my_vector[0])
print(my_vector[6])
print(my_vector[-7])

# we get some properties
print(my_vector.size)
print(my_vector.ndim)

# let create a array of 0 to 34 int array
my_array = np.arange(35)
print(f'my_array.shape is {my_array}')
# reshape it into a 2d
shape_size = (7, 5)
my_array.shape = shape_size
print(f'my_array.shape after reshaping is {my_array}')

# we can access an individual
row = 2
col = 3
print(f'The element at (2,3) is  {my_array[row, col]}')
# we can access a row of the 2d array by
print(f'The row 2 is\n{my_array[row]}\n')
# we can access a column by
print(f'The column 3 is\n {my_array[:, col]}\n')
#we can access group of columns
print(f'The column 0,2 is\n{my_array[:, [0, 2]]}\n')

# let us show the same on 3d array
print('\nA 3d example\n')
x_size = 3
y_size = 4
z_size = 5
shape_size = (x_size, y_size, z_size)
my_3d_array = np.arange(x_size*y_size*z_size)
my_3d_array.shape = shape_size
print(f'my_3d_array is \n {my_3d_array}')


import numpy as np

x = np.array([[1, 2], [3, 4], [5, 6]])
y = x[[0,1,2], [0,1,0]]
print (y)

import numpy as np
x = np.array([[ 0,  1,  2],[ 3,  4,  5],[ 6,  7,  8],[ 9, 10, 11]])

print ('Our array is:' )
print (x)
print ('\n')

rows = np.array([[0,0],[3,3]])
cols = np.array([[0,2],[0,2]])
y = x[rows,cols]

print ('The corner elements of this array are:')
print (y)

import numpy as np
x = np.array([[ 0,  1,  2],[ 3,  4,  5],[ 6,  7,  8],[ 9, 10, 11]])

print ('Our array is:')
print (x)
print ('\n')

# slicing
z = x[1:4,1:3]

print ('After slicing, our array becomes:')
print (z)
print ('\n')

# using advanced index for column
y = x[1:4,[1,2]]

print ('Slicing using advanced index for column:')
print (y)

# Boolean Array Indexing
import numpy as np
x = np.array([[ 0,  1,  2],[ 3,  4,  5],[ 6,  7,  8],[ 9, 10, 11]])

print ('Our array is:')
print (x)
print ('\n')

# Now we will print the items greater than 5
print ('The items greater than 5 are:')
print (x[x > 5])

import numpy as np
a = np.array([np.nan, 1,2,np.nan,3,4,5])
print (a[~np.isnan(a)])

import numpy as np
a = np.array([1, 2+6j, 5, 3.5+5j])
print (a[np.iscomplex(a)])