import numpy as np

def plane_coeff(x, y, z):
    # Credit: amroamroamro gist on github
    x = np.asarray(x)
    y = np.asarray(y)
    z = np.asarray(z)
    A = np.c_[x.ravel(), y.ravel(), np.ones(z.ravel().size)]
    C, _, _, _ = np.linalg.lstsq(A, z.ravel(), rcond=None)  # coefficients
    return C

def cartesian_gradient(f, x, y):
    """
    f : 2d array
    x : 1d array (colums)
    y : 1d array (rows)

    """
    f = np.asarray(f)
    x = np.asarray(x)
    y = np.asarray(y)

    nr, nc = f.shape
    if (nr != len(y)) or (nc != len(x)):
        raise ValueError("y and x are expected to be rows and columns respectively")

    dfdy = np.gradient(f, y, axis=0)
    dfdx = np.gradient(f, x, axis=1)

    return dfdx, dfdy

def spherical_polar_gradient(f, lon, lat, r=6371000.0):
    """
    f : scalar array
    lon : 1d array -180 to 180
    lat : 1d array -90 to 90
    Doesn't deal with the dateline...?
    """
    f = np.asarray(f)
    lon = np.deg2rad(np.asarray(lon))
    lat = np.deg2rad(np.asarray(lat))

    nr, nc = f.shape
    if (nr != len(lat)) or (nc != len(lon)):
        raise ValueError(
            "Latitude and longitude are expected to be rows and columns respectively"
        )

    _, latg = np.meshgrid(lon, lat)

    # Cosine because latitude from -pi/2 to pi/2. Not 0 to pi.
    dfdlat = np.gradient(f, lat, axis=0) / r
    dfdlon = np.gradient(f, lon, axis=1) / (r * np.cos(latg))

    return dfdlon, dfdlat

def haversine(theta):
    return 0.5 * (1 - np.cos(theta))

def archaversine(y):
    return np.arccos(1 - 2 * y)

def haversine_distance(lon0, lat0, lon1, lat1, r=6371000.0):
    """Calculates the distance between longitude and latitude coordinates on a
    spherical earth with radius using the Haversine formula.

    Parameters
    ----------
    lon0 : 1d numpy array
        Longitude values. [degrees]
    lat0 : 1d numpy array
        Latitude values. [degrees]
    lon1 : 1d numpy array
        Longitude values. [degrees]
    lat1 : 1d numpy array
        Latitude values. [degrees]

    Returns
    -------
    dist : 1d numpy array
        Distance between lon and lat positions. [m]

    """

    lon0 = np.deg2rad(lon0)
    lat0 = np.deg2rad(lat0)
    lon1 = np.deg2rad(lon1)
    lat1 = np.deg2rad(lat1)

    dist = r * archaversine(
        haversine(lat1 - lat0) + np.cos(lat1) * np.cos(lat2) * haversine(lon1 - lon0)
    )

    return dist

# Projecting to UTM with pyproj: https://gist.github.com/twpayne/4409500


# -*- coding: utf-8 -*-
"""
Created on Tue Dec 14 11:05:25 2021

@author: Oscar
"""

"Tutorial 10 "
import math as m
import sympy as sp
import numpy as np
import matplotlib.pyplot as plt

#%% Question 1: Deflection of a beam
# A) use numerical integration to compute the deflection

# Define the variables and equations

# Length of beam
L = 3 #m
# interval size
delta_x = 0.125 #m
interval = L/delta_x
# Modulus of Elasticity
E = 2e11 # 200 Gpa ~ 2e5 MPa
# Moment of inertia
I = 3e-4 # m^4
# Distributed load
w0 = 250000 # 2.5N/cm or 250000N/m

# Define Functions using Sympy
x = sp.Symbol('x')
f = (w0*(-5 * x**4 + (6 * L**2) * x**2 -L**4))/(120*E*I*L)
theta = sp.lambdify(x, f)

# Function to perform calculations
def SimpsonsThird(lower_limit, upper_limit, interval):

    interval_size = (float(upper_limit - lower_limit) / interval) #dx or h`
    sum = theta(lower_limit) + theta(upper_limit);

    # Calculates value till integral limit
    for i in range(1, interval ):

        if (i % 3 == 0):
            sum = sum + 2 * theta(lower_limit + i * interval_size)
        else:
            sum = sum + 3 * theta(lower_limit + i * interval_size)

    return ((float( 3 * interval_size) / 8 ) * sum )

integral_res = SimpsonsThird(0, L/2, 24)
print('The value of the deflection at the midpoint of the beam (x = 1.5 m) is ', integral_res)

#%% Question 1B): Numerical differention to computed moment and shear

# Moment = d/dx (theta)*EI
# Shear =d/dx (Moment)

g = sp.diff(f, x)
Moment = sp.lambdify(x, g)
solution = Moment(3)*(E*I)
print('The Moment is %.2f' %solution, 'at x = 3 m.')

v = sp.diff(f, x, 2)
Shear = sp.lambdify(x, v)
force = Shear(3)*E*I
print('The Shear is %.2f' %force, 'at x = 3 m ')


import numpy as np
from scipy import integrate, optimize
import matplotlib.pyplot as plt

G1, G2, k1, k2, alpha1, alpha2, N0 = np.array([3.0, 2.0, 1.0, 1.0, 0.5, 0.5, 3.0])
def f(Y, t):
    y1, y2 = Y
    return [G1*(N0 - (alpha1*y1) - (alpha2*y2))*y1 - k1*y1, G2*(N0 - (alpha1*y1) - (alpha2*y2))*y2 - k2*y2]

m1, m2 = np.array([10, 10])
y1 = np.linspace(-m1, m1, 20)
y2 = np.linspace(-m2, m2, 20)

Y1, Y2 = np.meshgrid(y1, y2)

t = 0

u, v = np.zeros(Y1.shape), np.zeros(Y2.shape)

NI, NJ = Y1.shape

for i in range(NI):
    for j in range(NJ):
        x = Y1[i, j]
        y = Y2[i, j]
        yprime = f([x, y], t)
        u[i,j] = yprime[0]
        v[i,j] = yprime[1]

Q = plt.quiver(Y1, Y2, u, v, color='r')

plt.xlabel('$n_1$')
plt.ylabel('$n_2$')
plt.xlim([-m1, m1])
plt.ylim([-m2, m2])
plt.show()


import numpy as np
from scipy.spatial.distance import cdist

class KMeans:
    def __init__(
            self,
            k: int,
            metric: str = "euclidean",
            tol: float = 1e-6,
            max_iter: int = 100,
            n_restarts: int = 10):
        """
        inputs:
            k: int
                the number of centroids to use in cluster fitting
            metric: str
                the name of the distance metric to use
            tol: float
                the minimum error tolerance from previous error during optimization to quit the model fit
            max_iter: int
                the maximum number of iterations before quitting model fit
            n_restarts: int
                the number of random initializations the algorithm will conduct
        """
        assert(k > 0), "k must be greater than 0"
        self._k = k
        self._metric = metric
        self._tol = tol
        self._max_iter = max_iter
        self._centroids = None
        self._error = np.inf
        self._n_restarts = n_restarts

    def fit(self, mat: np.ndarray):
        """
        fits the kmeans algorithm onto a provided 2D matrix

        inputs:
            mat: np.ndarray
                A 2D matrix where the rows are observations and columns are features
        """

        # try n_restart initializations of the centroids
        for j in range(self._n_restarts):
            # initialize each centroid at a data point
            centroids = mat[np.random.choice(mat.shape[0], self._k, replace = False)]
            errors = np.zeros(self._max_iter)
            it = 0
            # run the EM algorithm for max_iter iterations
            for it in range(self._max_iter):
                distances = cdist(mat, centroids, self._metric) # compute distances from each point to each centroid
                assignments = np.argmin(distances, axis = 1) # assign each point to closest centroid
                # update centroid positions
                for cluster in range(self._k):
                    centroids[cluster, :] = np.mean(mat[assignments == cluster,:], axis = 0)
                errors[it] = np.mean(np.linalg.norm(centroids[assignments,:] - mat, axis = 1))
                if it > 0:
                    if errors[it] - errors[it - 1] < self._tol: # optimization complete
                        break
            # if the lowest error was achieved, save the centroid positions and the error
            if errors[it] < self._error:
                self._centroids = centroids
                self._error = errors[it]

    def predict(self, mat: np.ndarray) -> np.ndarray:
        """
        predicts the cluster labels for a provided 2D matrix

        inputs:
            mat: np.ndarray
                A 2D matrix where the rows are observations and columns are features

        outputs:
            np.ndarray
                a 1D array with the cluster label for each of the observations in `mat`
        """
        distances = cdist(mat, self._centroids, self._metric)
        return np.argmin(distances, axis = 1)

    def get_error(self) -> float:
        """
        returns the final squared-mean error of the fit model

        outputs:
            float
                the squared-mean error of the fit model
        """
        return self._error

    def get_centroids(self) -> np.ndarray:
        """
        returns the centroid locations of the fit model

        outputs:
            np.ndarray
                a `k x m` 2D matrix representing the cluster centroids of the fit model
        """
        return self._centroids


"""util

sigmoid, binary_cross_entropy, softmax, cross_entropy, kappa are implemented.

"""
import numpy as np

EPS = 1e-20

def _log(x):
    """_log

    to prevent np.log_log(0), caluculate np.log(x + EPS)

    Args:
        x (array)

    Returns:
        array: same shape as x, log equals np.log(x + EPS)

    """
    if np.any(x < 0):
        print("log < 0")
        exit()
    return np.log(x + EPS)

def sigmoid(logit):
    """sigmoid function

    Args:
        logit (array) : data

    Returns:
        array: same shape as logit

    """
    prob = np.zeros_like(logit)
    prob[logit >= 0] = 1/(1 + np.exp(-logit[logit >= 0]))
    prob[logit < 0] = np.exp(logit[logit < 0])/(np.exp(logit[logit < 0]) + 1)
    return prob

def binary_cross_entropy(target,pred):
    """binary cross entropy

    Args:
        target (2-D array) : shape = (N_samples,1), value should be 0 or 1
        pred (2-D array) : shape = (N_samples,1), value shoule be in (0,1)

        or

        target (2-D array) : shape = (N_samples,2), onehotencoding
        pred (2-D array) : shape = (N_samples,2), value shoule be int (0,1)

    Returns:
        float: mean of loss in records

    """
    if target.shape[1] == 1:
        loss = -target*_log(pred) - (1.0 - target)*_log(1.0 - pred)
    else:
        loss = target*_log(pred)
        loss = -loss.sum(axis = 0)
    return loss.mean()

def softmax(x):
    """softmax function

    Args:
        x (2-D array) : shape = (N_samples,N_class)

    Returns:
        2-D array: shape = (N_samples,N_class)

    """

    row_max = x.max(axis = 1).reshape(-1,1)
    x -= row_max
    ratio = np.exp(x)
    total = ratio.sum(axis = 1).reshape(-1,1)
    return ratio/total

def cross_entropy(target,pred):
    """cross entropy

    Args:
        target (2-D array) : shape = (N_samples,N_class) onehotencoding
        pred (2-D array) : shape = (N_samples,N_class) value shouled be in (0,1)

    Returns:
        float: mean of loss in records

    """
    loss = target*_log(pred)
    return -loss.mean()

def kappa(sigma):
    """kappa

    this is used when approximating the inverse function of a probit function

    Args:
        sigma (array): sigma

    Returns:
        array: 1/sqrt(1 + \pi*sigma/8)

    """
    return (1 + np.pi*sigma/8)**(-0.5)


# Problem: https://projecteuler.net/problem=683

"""
. Same idea as problem 227.
. Use the distance between the dice as a state.
. Use Markov chain to track the probabilities.

. E[(X_2)^2 +(X_3)^2 + ... + (X_n)^2] = E[(X_2)^2] + E[(X_3)^2] + ... + E[(X_n)^2] (linearity of expectation)

. E(X^2) = [E(X)]^2 + Var(X)

. Use the formula here to find Var(X) and E(X) from the fundamental matrix,
    https://en.wikipedia.org/wiki/Absorbing_Markov_chain#Expected_number_of_steps
"""

import numpy as np

N = 500

def E_X2(N):
    ans = 0.0

    T = np.zeros((N, N), dtype = np.double)

    T[0][0] = 0
    for delta in range(1, N):
        # player 1 rolls [1,2], player 2 rolls [1,2]: delta doesn't change
        # player 1 rolls [1,2], player 2 rolls [3,4]
        T[delta][(delta-1)%N] += 4/36
        # player 1 rolls [1,3], player 2 rolls [5,6]
        T[delta][(delta-2)%N] += 4/36

        # player 1 rolls [3,4], player 2 rolls [1,2]
        T[delta][(delta+1)%N] += 4/36

        # player 1 rolls [3,4], player 2 rolls [5,6]
        T[delta][(delta-1)%N] += 4/36

        # player 1 rolls [5,6], player 2 rolls [1,2]
        T[delta][(delta+2)%N] += 4/36
        # player 1 rolls [5,6], player 2 rolls [3,4]
        T[delta][(delta+1)%N] += 4/36
        # player 1 rolls [5,6], player 2 rolls [5,6]: delta doesn't change

        T[delta][delta] += 1.0 - np.sum(T[delta])

    fundamental_matrix = np.linalg.inv(np.eye(N) - T)

    #t = fundamental_matrix.dot(np.ones((N,1)))
    t = np.sum(fundamental_matrix, axis = 1)
    E_steps_per_state = t-1
    E2_X = E_steps_per_state*E_steps_per_state

    V_steps_per_starting_state = (2*fundamental_matrix - np.eye(N)).dot(t) - t * t
    V_X = V_steps_per_starting_state

    ans = np.sum(E2_X + V_X) / N

    return ans

def G(N):
    ans = 0.0
    for i in range(2, N+1):
        ans += E_X2(i)
    return ans

if __name__ == "__main__":
    print(G(N))


# Code for solving the 1+1 dimensional diffusion equation
# du/dt = ddu/ddx on a rectangular grid of size L x (T*dt),
# with with L = 1, u(x,0) = g(x), u(0,t) = u(L,t) = 0

import numpy, sys, math
from  matplotlib import pyplot as plt
import numpy as np

def forward_step(alpha,u,uPrev,N):
    """
    Steps forward-euler algo one step ahead.
    Implemented in a separate function for code-reuse from crank_nicolson()
    """

    for x in xrange(1,N+1): #loop from i=1 to i=N
        u[x] = alpha*uPrev[x-1] + (1.0-2*alpha)*uPrev[x] + alpha*uPrev[x+1]

def forward_euler(alpha,u,N,T):
    """
    Implements the forward Euler sheme, results saved to
    array u
    """

    #Skip boundary elements
    for t in xrange(1,T):
        forward_step(alpha,u[t],u[t-1],N)

def tridiag(alpha,u,N):
    """
    Tridiagonal gaus-eliminator, specialized to diagonal = 1+2*alpha,
    super- and sub- diagonal = - alpha
    """
    d = numpy.zeros(N) + (1+2*alpha)
    b = numpy.zeros(N-1) - alpha

    #Forward eliminate
    for i in xrange(1,N):
        #Normalize row i (i in u convention):
        b[i-1] /= d[i-1];
        u[i] /= d[i-1] #Note: row i in u = row i-1 in the matrix
        d[i-1] = 1.0
        #Eliminate
        u[i+1] += u[i]*alpha
        d[i] += b[i-1]*alpha
    #Normalize bottom row
    u[N] /= d[N-1]
    d[N-1] = 1.0

    #Backward substitute
    for i in xrange(N,1,-1): #loop from i=N to i=2
        u[i-1] -= u[i]*b[i-2]
        #b[i-2] = 0.0 #This is never read, why bother...

def backward_euler(alpha,u,N,T):
    """
    Implements backward euler scheme by gaus-elimination of tridiagonal matrix.
    Results are saved to u.
    """
    for t in xrange(1,T):
        u[t] = u[t-1].copy()
        tridiag(alpha,u[t],N) #Note: Passing a pointer to row t, which is modified in-place

def crank_nicolson(alpha,u,N,T):
    """
    Implents crank-nicolson scheme, reusing code from forward- and backward euler
    """
    for t in xrange(1,T):
        forward_step(alpha/2,u[t],u[t-1],N)
        tridiag(alpha/2,u[t],N)

def g(x):
    """Initial condition u(x,0) = g(x), x \in [0,1]"""
    return numpy.sin(math.pi*x)

# Number of integration points along x-axis
    N       =   100
# Step length in time
    dt      =   0.01
# Number of time steps till final time
    T       =   100
# Define method to use 1 = explicit scheme, 2= implicit scheme, 3 = Crank-Nicolson
    method  =   2

#dx = 1/float(N+1)
u = numpy.zeros((T,N+2),numpy.double)
(x,dx) = numpy.linspace (0,1,N+2, retstep=True)
alpha = dt/(dx**2)

#Initial codition
u[0,:] = g(x)
u[0,0] = u[0,N+1] = 0.0 #Implement boundaries rigidly

if   method == 1:
    forward_euler(alpha,u,N,T)
elif method == 2:
    backward_euler(alpha,u,N,T)
elif method == 3:
    crank_nicolson(alpha,u,N,T)
else:
    print "Please select method 1,2, or 3!"
    import sys
    sys.exit(0)

# add on how to make a movie of the results


import numpy as np

def euclidean_distance(x1, x2):
    return np.sqrt(np.sum((x1 - x2) ** 2))

class KMeans:
    def __init__(self, K=2, max_iters=200):
        self.K = K
        self.max_iters = max_iters

        self.clusters = [[] for _ in range(self.K)]
        self.centroids = []

    def fit(self, X):
        self.X = X
        self.n_samples, self.n_features = X.shape

        random_sample_idxs = np.random.choice(self.n_samples, self.K, replace=False)
        self.centroids = [self.X[idx] for idx in random_sample_idxs]

        for _ in range(self.max_iters):
            self.clusters = self._create_clusters(self.centroids)

            centroids_old = self.centroids
            self.centroids = self._get_centroids(self.clusters)

            if self._is_converged(centroids_old, self.centroids):
                break

        return self._get_cluster_labels(self.clusters),self.centroids

    def _get_cluster_labels(self, clusters):
        labels = np.empty(self.n_samples)

        for cluster_idx, cluster in enumerate(clusters):
            for sample_index in cluster:
                labels[sample_index] = cluster_idx
        return labels

    def _create_clusters(self, centroids):
        clusters = [[] for _ in range(self.K)]
        for idx, sample in enumerate(self.X):
            centroid_idx = self._closest_centroid(sample, centroids)
            clusters[centroid_idx].append(idx)
        return clusters

    def _closest_centroid(self, sample, centroids):
        distances = [euclidean_distance(sample, point) for point in centroids]
        closest_index = np.argmin(distances)
        return closest_index

    def _get_centroids(self, clusters):
        centroids = np.zeros((self.K, self.n_features))
        for cluster_idx, cluster in enumerate(clusters):
            cluster_mean = np.mean(self.X[cluster], axis=0)
            centroids[cluster_idx] = cluster_mean
        return centroids

    def _is_converged(self, centroids_old, centroids):
        distances = [
            euclidean_distance(centroids_old[i], centroids[i]) for i in range(self.K)
        ]
        return sum(distances) == 0

    def distance(self,x, cen):
        Xs=x
        cent=cen
        dis=[]
        for i in range(len(cent)):
            summ=[]
            for j in Xs[i]:
                summ.append((j-cent[i]) ** 2)
            dis.append(sum(summ))
        return dis

    def inertia_(self):
        clus=self._get_cluster_labels(self.clusters)
        cen=self.centroids
        x=[]
        uni=np.unique(clus)
        for i in range(self.K):
            a=[]
            for j in range(len(clus)):
                if clus[j]==uni[i]:
                    a.append(self.X[j].tolist())
            x.append(a)
        s=sum(self.distance(x,cen))
        return s


# A little library for the generation and attribute checking of
# matrix of integers
# author : Etienne THIERY

from numpy import *

def symmetricPositiveDefinite(n, maxValue= 1):
    ''' Generates a n x n random symmetric, positive-definite matrix.
    The optionnal maxValue argument can be used to specify a maximum
    absolute value for extradiagonal coefficients.
    Diagonal coefficient will be inferior to 22 times maxValue in
    absolute value.

    Runs in O(n^2)'''

    # To generate such a matrix we use the fact that a symmetric
    # diagonnaly dominant matrix is symmetric positive definite

    # We first generate a random matrix
    # with coefficients between -maxValue and +maxValue
    A = random.random_integers(-maxValue, maxValue, (n, n))

    # Then by adding to this matrix its transpose, we obtain
    # a symmetric matrix
    A = A + A.transpose()

    # Finally we make sure it is strictly diagonnaly dominant by
    # adding 2*n*maxValue times the identity matrix
    A += 2*n*maxValue*eye(n)
    return A

def symmetricSparsePositiveDefinite(n, nbZeros, maxValue= 1):
    ''' Generates a n x n random symmetric, positive-definite matrix.
    with around nbZeros null coefficients (more precisely nbZeros+-1)
    nbZeros must be between 0 and n*(n-1)
    The optionnal maxValue argument can be used to specify a maximum
    absolute value for extradiagonal coefficients.
    Diagonal coefficient will be inferior to 11 times maxValue in
    absolute value.

    Runs in O(n^2)'''

    # The algorithm is the same as in symmetricPositiveDefinite
    # except that the matrix generated in the beginning is
    # sparse symmetric

    A = zeros((n,n))
    currentNbZeros = n*(n-1)
    while currentNbZeros > nbZeros:
        i, j = random.randint(n, size=2)
        if i != j and A[i,j] == 0:
            while A[i,j] == 0:
                A[i,j] = A[j,i] = random.randint(-maxValue, maxValue+1)
            currentNbZeros -= 2

    # Then we make sure it is strictly diagonnaly dominant by
    # adding n*maxValue times the identity matrix
    A += n*maxValue*eye(n)
    return A

def isSymmetric(M):
    ''' Returns true if and only if M is symmetric'''
    return array_equal(M, M.transpose())

def isDefinitePositive(M):
    ''' Returns true if and only if M is definite positive'''
    # using the fact that if all its eigenvalues are positive,
    # M is definite positive
    # be careful, as eigvals use numerical methods, some eigenvalues
    # which are in reality equal to zero can be found negative
    eps = 1e-5
    for ev in linalg.eigvals(M):
        if ev <= 0-eps:
            return False
    return True


## 4/9/2018 Knapsack Bo Lin
import numpy as np

def knapsack_0_1(weight,value,max_weight):
    num = len(weight) ## get the number of distinct items
    dp = np.zeros((num,max_weight+1)) ## initialize dp matrix
    ## transfer function dp[i][j]=max(f[i-1][j],f[i-1][j-weight[i]]+value[i])。
    for i in range(num):
        for j in range(max_weight+1):
            if weight[i] > j:
                dp[i][j] = dp[i-1][j]
            else:
                dp[i][j] =  max(dp[i-1][j],dp[i-1][j-weight[i]]+ value[i])
    max_value = np.amax(dp).astype(int).astype(str)
    return max_value,dp

def find_item(max_weight,dp,weight): ## find the item
    j = max_weight
    num = len(weight)
    in_pack = np.zeros((1,num))## try to find which one is put in the pack
    for i in reversed(range(num)):
        if i != 0 and (dp[i][j] > dp[i-1][j]):
            in_pack[0][i] = 1
            j = j - weight[i]
        if i == 0 and (dp[i][j] > 0):
            in_pack[0][i] = 1
    return in_pack

#test
weight = [1,2,2,3,4,5,6,3,5]
value  = [3,5,2,2,4,3,4,9,4]
max_weight = 13
max_value,dp = knapsack_0_1(weight,value,max_weight)
in_pack = find_item(max_weight,dp,weight)

print('The maximum value is ' + max_value)
print(dp)
print('Weight = ' , weight,'actual weight = ',np.sum(weight*in_pack))
print('Value = ',value,'actual value = ',np.sum(value*in_pack))
print('In_pack= ',in_pack)


import sympy as sp
from IPython.display import display
sp.init_printing()  # pretty printing

# Define impulse and unit step as functions of t
t = sp.symbols('t');
imp = sp.DiracDelta(t);
ustep = sp.Heaviside(t);
#ustep = sp.Piecewise( (0, t<1), (1, True));  # diff() doesn't give delta function?!

# Setup differential equation
x = sp.Function('x');  y = sp.Function('y');
RC = sp.symbols('RC');#, real=True);
lp1de = sp.Eq(y(t) + RC*sp.diff(y(t), t), x(t));
#print(lp1de);  display(lp1de);

# Generic solution
y_sl0e = sp.dsolve(lp1de, y(t));
y_sl0r = y_sl0e.rhs  # take only right hand side
#print(y_sl0e);  display(y_sl0e);

# Initial condition
a0 = sp.symbols('a0');
cnd1 = sp.Eq(y_sl0r.subs(t, -1), a0);  # y(-1) = a0
#cnd2 = sp.Eq(y_sl0r.diff(t).subs(t, -1), b0)  # y'(-1) = b0
#print(cnd1);  display(cnd1);

# Solve for C1:  magic brackets in solve() returns result as dictionary
C1 = sp.symbols('C1')  # generic constants
C1_sl = sp.solve([cnd1], (C1))
#C1C2_sl = sp.solve([cnd1, cnd2], (C1, C2))
#print(C1_sl);  display(C1_sl);

# Substitute back for solution in terms of a0
y_sl1 = y_sl0r.subs(C1_sl);
#print(sp.Eq(y(t), y_sl1));  display(sp.Eq(y(t), y_sl1));

# Set values for constants
y_sl1s = y_sl1.subs({RC:1,a0:0}).doit()
#print(sp.Eq(y(t), y_sl1s));  display(sp.Eq(y(t), y_sl1s));

# Set input function and solve
y_sl1sx = y_sl1s.subs({x(t):ustep}).doit()
print(sp.Eq(y(t), y_sl1sx));  display(sp.Eq(y(t), y_sl1sx))

# Plot output
sp.plot(y_sl1sx, (t,-4,8))


# By
# ████████╗██╗   ██╗ █████╗ ███╗   ██╗    ██╗  ██╗ ██████╗
# ╚══██╔══╝██║   ██║██╔══██╗████╗  ██║    ██║  ██║██╔═══██╗
#    ██║   ██║   ██║███████║██╔██╗ ██║    ███████║██║   ██║
#    ██║   ██║   ██║██╔══██║██║╚██╗██║    ██╔══██║██║   ██║
#    ██║   ╚██████╔╝██║  ██║██║ ╚████║    ██║  ██║╚██████╔╝
#    ╚═╝    ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═══╝    ╚═╝  ╚═╝ ╚═════╝

# Email: ttuan.ho@outlook.com

from scipy.stats import norm, binom, poisson, expon, uniform
from scipy import stats
import numpy as np
from scipy.stats import ttest_ind
import statsmodels.api as sm
import pylab as py
"""
Ex
"""

"""
Ex3:
Consider again the astronaut exercise (Exercise 4) from the week 5 exercises. The change in heart
rate (in beats per minute) is as follows:
15 32.5 49 26 8 28
a) Is there evidence that astronaut pulse rate has in fact increased under simulated weightlessness? Use a hypothesis test to answer this question, via Matlab’s ttest function.
b) Is this consistent with your results from last week?
c) Produce a normal quantile plot of the data. How reasonable is the normality assumption for these
data?
"""
data=np.array([15,32.5,49,26,8,28])
n = len(data)
zeros = np.zeros((n,))
m = np.mean(data)
s = np.std(data, ddof=1)

# % matlab version:
# [h,p] = ttest(data, 0,'tail', 'right');p

# stat, p = ttest_ind(data, zeros)
# print('t=%.3f, p=%.3f' % (stat, p))

tObs = (m - 0) / (s / ((n)**(1/2)))
# % matlab version:
# pValue = tcdf(t,n-1)
# pValue in python
pValue = stats.t.sf(tObs, n-1)
print(f"p-Value is {pValue}")

# produce qqplot of data
# % matlab version:
# qqplot(data)
sm.qqplot(data)
py.show()

import numpy,from scipy,import statsmodels


"""
This module contains the functions needed for the trigonometric interpolation.
"""

from typing import Tuple
import numpy as np
from numpy.typing import ArrayLike
import matplotlib.pyplot as plt

def e_complex_pow(power: float) -> complex:
    r"""
    Function which implements Euler's Formula for a real input number:
    :math:`e^{j \times power} = \cos(power) + j \times\sin(power)` .

    :param power: real number
    :return: complex number
    """
    return complex(np.cos(power), np.sin(power))

def get_complex_representation(points: ArrayLike) -> np.ndarray:
    r"""
    Function to calculate the Fourier coefficients of polynomial from its values,
    provided as an input parameter.

    Calculate following:
       - nth root of unity which is a complex number: :math:`w = e^{{2\pi i}/n}`
       - discrete Fourier transformation matrix:
       :math:`F = (w^{jk})_{0 \leq j \leq n-1, -(n-1)/2 \leq k \leq (n-1)/2}`

       - discrete Fourier coefficients :math:`c_k = (1/n) \times conjugate(F) \times y`

    :param points: polynomial values
    :return: complex Fourier coefficients
    """
    len_n = len(points)

    y_s = np.array(points, dtype=np.complex_)

    w_comp = e_complex_pow(2 * np.pi / len_n)
    # Fourier-Matrix
    f_matrix = np.array(
        [
            [w_comp ** (j * k) for j in range(len_n)]
            for k in range(-(len_n - 1) // 2, (len_n - 1) // 2 + 1)
        ]
    )

    ck_s = (1 / len_n) * (np.conj(f_matrix) @ y_s)  # Fourier-Coefficients
    return ck_s

def get_sin_cos_representation(points: ArrayLike) -> Tuple[np.ndarray, np.ndarray]:
    r"""
    Function to calculate sine and cosine coefficients.

    .. math::
     a_0=2 \times c_0  \\  a_k = c_k + c_{-k}  \\  b_k = j \times (c_k - c_{-k})

    :param points: polynomial values
    :return: sine and cosine coefficients
    """
    ck_s = get_complex_representation(points)
    ck_zero_idx = (len(ck_s) - 1) // 2

    # cosine coefficients
    ak_s = [2 * ck_s[ck_zero_idx]] + [
        ck_s[ck_zero_idx + n] + ck_s[ck_zero_idx - n] for n in range(1, ck_zero_idx + 1)
    ]
    # sine coefficients
    bk_s = [complex(0)] + [
        complex(0, ck_s[ck_zero_idx + n] - ck_s[ck_zero_idx - n])
        for n in range(1, ck_zero_idx + 1)
    ]

    for n_len, _ in enumerate(ak_s):
        if ak_s[n_len].imag < 1e-3:
            ak_s[n_len] = ak_s[n_len].real

        if bk_s[n_len].imag < 1e-3:
            bk_s[n_len] = bk_s[n_len].real

    return np.array(ak_s), np.array(bk_s)

def eval_sin_cos_representation(
    t_period: float, a_coeff: np.ndarray, b_coeff: np.ndarray
) -> float:
    r"""
    Function which calculates Fourier series from given period and coefficients.

    :math:`f(x) = a_0/2 + \sum_{n=1}^N(a_n\cos(n \times x) + b_n\sin(n \times x))`

    :param t_period: parameter of function `f(x)` which represent the point in the period of time
    :param a_coeff: cosine coefficients
    :param b_coeff: sine coefficients
    :return: Fourier series
    """
    return a_coeff[0] / 2 + sum(
        a_coeff[n] * np.cos(n * t_period) + b_coeff[n] * np.sin(n * t_period)
        for n in range(1, len(a_coeff))
    )

def plot_sin_cos_representation(
    a_coeff: np.ndarray,
    b_coeff: np.ndarray,
    y_points: ArrayLike,
    start: float = -10,
    end: float = 10,
) -> None:
    """
    Function to plot trigonometric Fourier series.

    :param a_coeff: cosine coefficients
    :param b_coeff: sine coefficients
    :param y_points: polynomial values
    :param start: starting value of the sequence on x axis
    :param end: end value of the sequence on x axis
    """
    x_s = np.linspace(start, end, 5000)
    y_s = [eval_sin_cos_representation(t, a_coeff, b_coeff) for t in x_s]

    n_len = len(y_points)
    x_points = np.array([(2 * np.pi * i) / n_len for i in range(n_len)])

    plt.figure(figsize=(14, 7))
    plt.plot(x_s, y_s)
    plt.scatter(x_points, y_points, c="black")
    plt.show()


# Please attribute to Llewelyn Richards-Ward,
#llewelyn62@icloud.com
#Use and distribute as you want.
from mayavi import mlab
import numpy as np
import matplotlib.pyplot as plt
import numba as nb
from scipy.integrate import odeint

#Create figure
mlab.figure(size=(900,800), bgcolor=(0,0,0), fgcolor=(1,1,1))
#For a very basic  approach which shows the steps this commented
#section can be used. eThe preferrend methods is to
#use odeint and an array/function combination, as below.
#Note the use of the Euler integration method.
#=========================================
#   # Integration time step
# dt = 0.001
#   # Lorenz ODE parameters
# sigma = 10.0
# r     = 28.0
# b     = 8.0/3.0
#   # Initial conditions
# x = -.5
# y =  .2
# z =  2.17
#   # Store the trajectory
# TrajX = []
# TrajY = []
# TrajZ = []
# Time  = []
#
#   # Integrate the Lorenz ODEs
#   #Pretty basic approach -- in real examples would
#   #use odeint and an array/function combination.
# for t in np.arange(0.,50.,dt):
# 	dxdt = sigma*(y - x)
# 	dydt = r * x - y - x * z
# 	dzdt = x * y - b * z
# 	x = x + dxdt * dt
# 	y = y + dydt * dt
# 	z = z + dzdt * dt
#
# 	TrajX.append(x)
# 	TrajY.append(y)
# 	TrajZ.append(z)
# 	Time.append(t)
#=========================================

# Lorenz paramters and initial conditions
sigma, beta, rho = 10, 8/3, 28
# #Very near C+ but not quite.
# u0, v0, w0 = np.sqrt(beta*(rho-1))+.01,np.sqrt(beta*(rho-1))+.01,27
# # Close to C+ but allowing of escape
# u0, v0, w0 =7.3,7.3,27
#Lorenz/Strogatz starting points
u0, v0, w0 = 0,1, 0

# Maximum time point and total number of time points
tmax, n = 50, 100000
#@nb.jit(nopython=False) #optimises for speed.
def deriv_lorenz(X, t, sigma, beta, rho):
    """The Lorenz equations."""
    x, y, z = X
    dx_dt = -sigma*(x - y)
    dy_dt = rho*x - y - x*z
    dz_dt = -beta*z + x*y
    return dx_dt, dy_dt, dz_dt

# Integrate the Lorenz equations on the time grid t
#Implements a detailed use of memory space for optimised accuracy, not
#so much speed.
#@nb.jit('float64(float64,float64,float64,float64,float64,float64,float64,float64)',nopython=False)
def int_f(u0=u0,v0=v0,w0=w0,sigma=sigma,beta=beta,rho=rho,tma=tmax,n=n):
    t = np.linspace(0, tmax, n)
    f = odeint(deriv_lorenz, (u0, v0, w0), t, args=(sigma, beta, rho))
    return f
x, y, z = int_f().T
Time = np.linspace(0,tmax,n)

mlab.plot3d(x,y,z,Time,colormap='Spectral',tube_radius=0.3)
mlab.outline(opacity=.4)
mlab.points3d(0,0,0,opacity=.5)
mlab.text3d(0,0,0,'Origin')
mlab.points3d(u0,v0,w0,opacity=.5)
mlab.text3d(u0,v0,w0,'t_0')
mlab.points3d(np.sqrt(beta*(rho-1)),np.sqrt(beta*(rho-1)),rho-1,opacity=.5)
mlab.text3d(np.sqrt(beta*(rho-1))*1.2,np.sqrt(beta*(rho-1)),rho-1,'C+')
mlab.points3d(-np.sqrt(beta*(rho-1)),-np.sqrt(beta*(rho-1)),rho-1,opacity=.5)
mlab.text3d(-np.sqrt(beta*(rho-1))*1.2,-np.sqrt(beta*(rho-1)),rho-1,'C-')
mlab.orientation_axes(xlabel='x',ylabel='y',zlabel='z')

#plt.show()
@mlab.animate(delay=50, ui=False)
def anim():
    f = mlab.gcf()
    while 1:
        f.scene.camera.azimuth(2)
        f.scene.render()
        yield

a = anim() # Starts the animation.
# mlab.savefig('Lorenz.png')


import numpy as np
from numpy import random as rnd

#-------Neighbour generation----------

def boltz_move(state, temp, interval):
    """
    The step length equals the square root of the current temperature.
    The direction is uniformly random.
    """

    new_state = [0,0]
    n = rnd.random()
    if n < 0.5 :
        new_state[0] = state[0] + np.sqrt(temp)
        new_state[1] = state[1] + np.sqrt(temp)
        return (clip(new_state[0], interval, state[0]),
                clip(new_state[1], interval, state[1]))
    else :
        new_state[0] = state[0] - np.sqrt(temp)
        new_state[1] = state[1] - np.sqrt(temp)
        return (clip(new_state[0], interval, state[0]),
                clip(new_state[1], interval, state[1]))

def clip(x, interval, state):
    """
    If x is not in interval,
    return a point chosen uniformly at random between the violated bound
    and the previous state;
    otherwise return x.
    """

    a,b = interval
    if x < a :
        return rnd.uniform(a, state)
    if x > b :
        return rnd.uniform(state, b)
    else:
        return x

#---------Acceptance function--------------

def boltz_acceptance_prob(energy, new_energy, temperature):
    """Boltzmann Annealing"""

    delta_e = new_energy - energy
    if delta_e < 0 :
        return 1
    else:
        return np.exp(- delta_e / temperature)

#--------Cooling Procedures---------------

def boltz_cooling(initial_temp, k):
    """Boltzmann temperature decreasing."""
    if k <= 1:
        return initial_temp
    else:
        return initial_temp / np.log(k)

def geom_cooling(temp, k,  alpha = 0.95):
    """Geometric temperature decreasing."""
    return temp * alpha

#--------Stopping Condition------------

def tolerance(energies, tolerance, tolerance_iter) :
    """
    The algorithm runs until the average change in value of the objective function
    is less than the tolerance.
    """

    if len(energies) <= tolerance_iter :
        return False
    if avg_last_k_value(energies, tolerance_iter) < tolerance :
        return True
    else :
        return False

def objective_limit(energy, limit):
    """
    The algorithm stops as soon as the current objective function value
    is less or equal then limit.
    """

    if energy <= limit :
        return True
    else :
        return False

def avg_last_k_value(energies, k):
    """
    Compute the average of the last k absolute differences between the values of a list.
    """

    diff = []
    L = len(energies)
    for i in range(L - 1,L - (k+1),-1):
        diff.append(abs(energies[i]-energies[i-1]))
    return np.mean(diff)

#------------------------------------------------------
#------------Simulated Anneanling algorithm------------
#------------------------------------------------------

def SA(cooling, acceptance_prob, energy, move, interval, initial_temp = 100.,
       k_max = 1e10, tolerance_value = 1e-6, tolerance_iter = 10,
       obj_fn_limit = -1e10, reann_tol = 100, verbose = False):

    #Step 1
    states = []
    energies = []
    temperatures = []
    s = (rnd.uniform(interval[0], interval[1]), rnd.uniform(interval[0], interval[1]))
    k = 0
    T = initial_temp
    reann = False
    exit_types = {0 :'Max Iter',
            1 : 'Tolerance',
            2 : 'Obj Limit',
            3 : 'Temp Limit'}
    _exit = 0

    if verbose:
        dash = '-' * 70
        print("\n")
        print ('{:_^70}'.format('Simulated Annealing'))
        print("Test function", energy)
        print("Initial state: {}".format(s))
        print("\n")

    while True:
        k += 1

        #Stopping criterion
        if k == k_max :
            if verbose :
                print(dash)
                print("MAX ITERATION EXIT")
                print(dash)
            break

        #Step 2
        new_s = move(s, T, interval)
        energy_s = energy(s)
        energy_new_s = energy(new_s)
        states.append(s)
        energies.append(energy_s)
        temperatures.append(T)

        #Step 3
        T = cooling(T, k)

        #Stopping criteria
        if T <= 0. :
            if verbose :
                print(dash)
                print("TEMPERATURE EXIT")
                print(dash)
            _exit = 3
            break

        if tolerance(energies, tolerance_value, tolerance_iter) :
            if verbose :
                print(dash)
                print("TOLERANCE EXIT")
                print(dash)
            _exit = 1
            break

        if objective_limit(energy_s, obj_fn_limit) :
            if verbose :
                print(dash)
                print("OBJECTIVE FUNCTION LIMIT EXIT")
                print(dash)
            _exit = 2
            break

        #Reanniling Process
        best_e = min(energies)
        best_s = states[np.argmin(energies)]

        if energy_s > best_e + reann_tol :
            if verbose :
                print(dash)
                print("REANNILING")
                print(dash)
            energies = []
            states = []
            s = best_s
            energy_s = best_e
            T = initial_temp
            k = 0
            reann = True
            continue

        #Step 4
        if acceptance_prob(energy_s, energy_new_s, T) >= rnd.random() :
            s = new_s

    return states, energies, temperatures, k, exit_types[_exit], reann


import numpy as np
from problem_3a import inverse_iteration
from problem_3b import rayleigh_iter

def main():
	print("Inverse Iteration")
	print("-------------------\n\n")

	A = np.array([[6, 2, 1], [2, 3, 1], [1, 1, 1]])
	x0 = np.array([1, 4, 2])
	print("Initial Array:\n\t", x0, "'\n")
	l, x, iters = inverse_iteration(A, x0)

	print("Computed Eigenvalue:", l)
	print("Computed Eigenvector:\n\t", x, "'\n")
	print("Number of iterations:", iters)

	print("\n\n")

	print("Rayleigh Quotient Iteration")
	print("------------------------------\n\n")

	A = np.array([[6, 2, 1], [2, 3, 1], [1, 1, 1]])
	x0 = np.array([1, 4, 2])
	print("Initial Array:\n\t", x0, "'\n")
	l_r, x_r, iters_r = rayleigh_iter(A, x0)

	print("Computed Eigenvalue:", l_r)
	print("Computed Eigenvector:\n\t", x_r, "'\n")
	print("Number of iterations:", iters_r)
	print("\n\n")

	print("Picking Rayleigh Quotient Iteration result as true value...\n\n")

	rel_err_vec = np.linalg.norm(x - x_r, ord=2) / np.linalg.norm(x_r, ord=2)
	rel_err_val = np.abs(l - l_r) / np.abs(l_r)

	print("Relative Error for Eigenvector:", rel_err_vec)
	print("Relative Error for Eigenvalue:", rel_err_val)

if __name__ == '__main__':
	main()


import numpy as np

def dct2(x):
    """
    :param x: 1D input signal in the time domain.
    :return: DCT(x) - 1D output signal in the frequency DCT domain.
    """
    P = x.shape[-1]
    X = np.zeros(P, dtype=float)
    for k in range(P):
        out = 0
        if k == 0:
            kk = 1. / np.sqrt(2.)
        else:
            kk = 1.
        for n in range(P):
            out += kk * x[n] * np.cos(np.pi * (n + .5) * k / P)
        out *= np.sqrt(2. / P)
        X[k] = out
    return X

def dct1(Y):
    N = Y.shape[-1]
    y = np.zeros(N, dtype=float)
    for n in range(N):
        out = 0
        for k in range(N):
            if k == 0:
                kk = 1. / 2.
            else:
                kk = 1.
            out += kk * Y[k] * np.cos(np.pi * n * k / N)
        y[n] = out
    return y

def correlate(x, h):
    """
    Cross-correlation of x and y via the DCT transform.

    :param x: input signal x in the time domain
    :param h: input filter y in the time domain
    :return: the output of the correlation
    """
    N = len(x)
    L = len(h)
    M = N + L - 1
    P1 = max((L - 3), 0) // 2 + 1
    P2 = max(P1 + 1, (N - 3) // 2 + 1)
    # P2 = (N - 3) // 2
    P = max(P2 + 1, 3 * M // 2 + 1)

    x = np.pad(array=x, pad_width=(P1, P - P1 - N), mode='constant')
    h = np.flip(h, axis=0)
    h = np.pad(array=h, pad_width=(P2, P - P2 - L), mode='constant')
    x = dct2(x)
    h = dct2(h)
    y = x * h
    y = dct1(y)[P1 + P2:P1 + P2 + M]
    return y

if __name__ == "__main__":
    x = np.array([1.0, -2.0, 3.0, -4.0, 2.0, -8.0, -1.0, 5.0], dtype=float)
    # x = np.random.randn(21)
    print("x: ", x)
    h = np.array([1.0, -4.0, 2.0], dtype=float)
    h_len = h.shape[-1]
    pad = (h_len - 1) // 2
    x_pad = np.pad(x, [pad, pad], mode="constant")
    expect = np.correlate(x_pad, h, mode='valid')
    print("expect: ", expect)
    result = correlate(x, h)
    print("result: ", result)
    assert np.testing.assert_allclose(actual=result, desired=expect)


'''
Implementing the technique in "A Smarter Way To Find Pitch" by Philip McLeod
and Geoff Wyvill, which is simple, elegant, and effective
'''
import numpy as np
import matplotlib.pyplot as plt

def estimateFundamentalFreq(x, doPlot = False):
    #Step 1: Compute normalized squared difference function
    #Using variable names in the paper
    N = x.size
    W = np.int(N/2)
    t = W
    corr = np.zeros(W)
    #Do brute force f FFT because I'm lazy
    #(fine because signals are small)
    for Tau in np.arange(W):
        xdelay = x[Tau::]
        L = (W - Tau)/2
        m = np.sum(x[int(t-L):int(t+L+1)]**2) + np.sum(xdelay[int(t-L):int(t+L+1)]**2)
        r = np.sum(x[int(t-L):int(t+L+1)]*xdelay[int(t-L):int(t+L+1)])
        corr[Tau] = 2*r/m

    #Step 2: Find the ''key max''
    #Compute zero crossings
    zc = np.zeros(corr.size-1)
    zc[(corr[0:-1] < 0)*(corr[1::] > 0)] = 1
    zc[(corr[0:-1] > 0)*(corr[1::] < 0)] = -1

    #Mark regions which are admissible for key maxes
    #(regions with positive zero crossing to left and negative to right)
    admiss = np.zeros(corr.size)
    admiss[0:-1] = zc
    for i in range(1, corr.size):
        if admiss[i] == 0:
            admiss[i] = admiss[i-1]

    #Find all local maxes
    maxes = np.zeros(corr.size)
    maxes[1:-1] = (np.sign(corr[1:-1] - corr[0:-2])==1)*(np.sign(corr[1:-1] - corr[2::])==1)
    maxidx = np.arange(corr.size)
    maxidx = maxidx[maxes == 1]
    maxTau = 0
    if len(corr[maxidx]) > 0:
        maxTau = maxidx[np.argmax(corr[maxidx])]

    if doPlot:
        plt.subplot(211)
        plt.plot(x)
        plt.title("Original Signal")
        plt.subplot(212)
        plt.plot(corr)
        plt.hold(True)
        plt.plot(admiss*1.05, 'r')
        plt.ylim([-1.1, 1.1])
        plt.scatter(maxidx, corr[maxidx])
        plt.scatter([maxTau], [corr[maxTau]], 100, 'r')
        plt.title("Max Tau = %i, Clarity = %g"%(maxTau, corr[maxTau]))
    return (maxTau, corr)

if __name__ == '__main__':
    T = 60
    NPeriods = 5
    np.random.seed(50)
    t = np.linspace(0, 2*np.pi*NPeriods, T*NPeriods)
    slope = t[1] - t[0]
    t = np.cumsum(np.random.rand(t.size)*2*slope)
    x = np.cos(t) + np.cos(2*t)
    f = estimateFundamentalFreq(x, True)


# encoding=utf8

"""
Module containing functions for calculating or approximating factorials.

"""

import numpy as np

from decimal import Decimal, localcontext

def factorial(n, prec=100):
    r"""
    Function for calculating factorials using the standard approach as explained in the
    Notes section. For factorials over 100, the decimal package is used to support the
    resulting large integers.

    Parameters
    ----------
    n : int
        The desired integer to calculate the factorial.
    prec : int default 100, optional
        Defines level of precision for factorials over 100
        for use by the decimal package

    Returns
    -------
    int or Decimal
        The computed factorial of the given integer.

    Notes
    -----
    Factorials are denoted for a positive integer :math:`x` as :math:`x!` and are
    defined as:

    .. math::

        x! = (x)(x - 1)(x - 2) \cdots (2)(1)

    For example, the factorial of 5 is written as:

    .. math::

        5! = (5)(4)(3)(2)(1) = 120

    Examples
    --------
    >>> factorial(10)
    3628800.0
    >>> factorial(50)
    3.0414093201713376e+64
    # Factorials above 100 use the decimal package to handle the resulting large integers
    >>> factorial(200)
    Decimal('7.886578673647905035523632139321850622951359776871732632947425332443594499634033429203042840119846238E+374')

    References
    ----------
    Press, W., Teukolsky, S., Vetterling, W., & Flannery, B. (2007). Numerical recipes (3rd ed.).
        Cambridge: Cambridge University Press.

    Weisstein, Eric W. "Factorial." From MathWorld--A Wolfram Web Resource.
        http://mathworld.wolfram.com/Factorial.html

    """
    if n != np.floor(n):
        n = np.floor(n)

    factor = np.arange(1, n + 1)

    if n > 100:
        with localcontext() as ctx:
            ctx.prec = prec
            f = Decimal(1)
            for i in reversed(factor):
                f = Decimal(f) * i
    else:
        f = float(1)
        for i in reversed(factor):
            f = f * i

    return int(f)

def stirlingln(n, keep_log=False, prec=100):
    r"""
    Approximates the factorial of n using the approximation
    given by Ramanujan in his lost notebook (Ramanujan 1988,
    as cited in Wikipedia). Computing the factorial in logarithmic
    form is useful as it helps avoid overflow when n is large. As
    values of n increase, the approximation given becomes more
    exact.

    Parameters
    ----------
    n : int
        The desired integer to calculate the factorial.
    keep_log : bool default False
        If True, the approximation remains in logarithmic
        form. If False, converts to exponent form before
        returning the factorial approximation.
    prec : int default 100, optional
        Defines level of precision for factorials over 100.

    Returns
    -------
    int or Decimal
        The computed log factorial of the given integer.

    Notes
    -----
    It is often useful to compute the logarithmic form of the
    factorial and convert it to exponent form to avoid overflow.
    The approximation is an alternative approach given by
    Srinivasa Ramanujan (Ramanujan 1988).

    .. math::

        ln n! \approx n ln n - n + \frac{1}{6} ln(n(1 + 4n(1 + 2n))) + \frac{1}{2} ln \pi

    Examples
    --------
    # Difference between actual factorial calculation and Stirling's Approximation
    # for low values of n is practically zero
    >>> (factorial(5) - stirlingln(5)) / stirlingln(5)
    3.8020354295010749e-06
    >>> stirlingln(50)
    3.041409303897981e+64
    >>> stirlingln(100)
    9.3326215380340829e+157
    # If the keep_log flag is set to True, the output remains in logarithmic form.
    >>> stirlingln(100, True)
    363.73937555488197

    References
    ----------
    Stirling's approximation. (2017, March 8). In Wikipedia, The Free Encyclopedia.
        From https://en.wikipedia.org/w/index.php?title=Stirling%27s_approximation&oldid=769328178

    """
    if n != np.floor(n):
        n = np.floor(n)

    n = float(n)
    if n > 100:
        with localcontext() as ctx:
            ctx.prec = prec
            f = Decimal(n * np.log(n) - n + (1. / 6.) * np.log(n * (1. + 4. * n * (1. + 2. * n))) + .5 * np.log(np.pi))
    else:
        f = n * np.log(n) - n + (1. / 6.) * np.log(n * (1. + 4. * n * (1. + 2. * n))) + .5 * np.log(np.pi)

    if keep_log is False:
        return np.exp(f)

    return f

def stirling(n, prec=100):
    r"""
    Approximates a factorial of an integer :math:`n` using Stirling's Approximation.
    Specifically, the approximation is done using a method developed by Gosper.

    Parameters
    ----------
    n : int
        The desired integer to calculate the factorial.
    prec
        Defines level of precision for factorials over 100. Default 100. Optional

    Returns
    -------
    int or Decimal
        The computed factorial of the given integer.

    Notes
    -----
    Stirling's approximation is a method of approximating a factorial :math:`n!`.
    As the value of :math:`n` increases, the more exact the approximation becomes;
    however, it still yields almost exact results for small values of :math:`n`.

    The approximation used is given by Gosper, which is noted to be a better
    approximation to :math:`n!` and also results in a very close approximation to
    :math:`0! = 1`.

    .. math::

        n! \approx \sqrt{(2n + \frac{1}{3})\pi} n^n e^{-n}

    Examples
    --------
    >>> stirling(0)
    1.0233267079464885
    >>> (factorial(5) - stirling(5)) / stirling(5)
    0.00024981097589214563
    >>> stirling(5)
    119.9700301696855
    >>> stirling(50)
    3.0414009581300833e+64

    References
    ----------
    Stirling's approximation. (2017, March 8). In Wikipedia, The Free Encyclopedia.
        From https://en.wikipedia.org/w/index.php?title=Stirling%27s_approximation&oldid=769328178

    Weisstein, Eric W. "Stirling's Approximation." From MathWorld--A Wolfram Web Resource.
        http://mathworld.wolfram.com/StirlingsApproximation.html

    """
    if n != np.floor(n):
        n = np.floor(n)

    if n >= 100:
        with localcontext() as ctx:
            ctx.prec = prec
            f = Decimal(np.sqrt((2. * n + 1. / 3.) * np.pi) * n ** n * np.exp(-n))
    else:
        f = np.sqrt((2. * n + 1. / 3.) * np.pi) * n ** n * np.exp(-n)

    return f

def ramanujan(n, prec=100):
    r"""
    Approximates the factorial :math:`n!` given an integer :math:`n` using Ramanujan's formula.
    Ramanujan's formula is just as or more accurate than several other factorial approximation
    formulas.

    Parameters
    ----------
    n
        Integer to approximate factorial
    prec
        Defines level of precision for factorials over 100. Default 100. Optional

    Returns
    -------
    int or Decimal
        Factorial of :math:`n` as approximated by Ramanujan's formula.

    Notes
    -----
    Ramanujan's formula is another factorial approximation method known for its accuracy
    in comparison to other factorial approximation approaches including Stirling's and
    Gosper's approximations. Ramanujan's formula is defined as:

    .. math::

        n! \approx \sqrt{\pi} \left(\frac{n}{e}\right)^n \sqrt[6]{8n^3 + 4n^2 + n + \frac{1}{30}}

    Examples
    --------
    >>> ramanujan(10)
    3628800.3116126074
    >>> ramanujan(5)
    120.00014706585664

    References
    ----------
    Mortici, Cristinel. On Gosper's Formula for the Gamma Function. Valahia University of Targoviste,
        Department of Mathematics. Retrieved from http://files.ele-math.com/articles/jmi-05-53.pdf

    """
    if n != np.floor(n):
        n = np.floor(n)

    if n >= 100:
        with localcontext() as ctx:
            ctx.prec = prec
            f = Decimal(
                np.sqrt(np.pi) * n ** n * np.exp(-n) * (8. * n ** 3. + 4. * n ** 2. + n + 1. / 30.) ** (1. / 6.))
    else:
        f = np.sqrt(np.pi) * n ** n * np.exp(-n) * (8. * n ** 3. + 4. * n ** 2. + n + (1. / 30.)) ** (1. / 6.)

    return f

def fallingfactorial(x, n):
    r"""
    Computes the falling factorial.

    Parameters
    ----------
    x
        Integer. The value will be rounded down if the value is not an integer.
    n
        Integer. The value will be rounded down if the value is not an integer.

    Returns
    -------
    int or str
        The falling factorial for an integer :math:`n`, :math:`(x)_{n}`. If x is a
        str, the output is the symbolic representation of the falling factorial.

    Notes
    -----
    The falling factorial, denoted as :math:`(x)_{n}` (or :math:`x^{\underline{n}}`) is
    defined as the following:

    .. math::

        (x)_n = x(x - 1) \cdots (x - (n - 1))

    The first few falling factorials are then:

    ..math::

        (x)_0 = 1
        (x)_1 = x
        (x)_2 = x(x - 1)
        (x)_3 = x(x - 1)(x - 2)
        (x)_4 = x(x - 1)(x - 2)(x - 3)

    Examples
    --------
    >>> fallingfactorial(10, 5)
    30240
    >>> fallingfactorial(10, 2)
    90
    >>> fallingfactorial('x', 2)
    'x*(x - 1)'
    >>> fallingfactorial('a', 4)
    'a*(a - 1)*(a - 2)*(a - 3)'

    References
    ----------
    Falling and rising factorials. (2017, June 8). In Wikipedia, The Free Encyclopedia.
        From https://en.wikipedia.org/w/index.php?title=Falling_and_rising_factorials&oldid=784512036

    Weisstein, Eric W. "Falling Factorial." From MathWorld--A Wolfram Web Resource.
        http://mathworld.wolfram.com/FallingFactorial.html

    """
    if n != np.floor(n):
        n = np.floor(n)

    if isinstance(x, str):
        f = x

        for i in np.arange(1, np.absolute(n)):
            f = f + '*(' + str(x) + ' - ' + str(i) + ')'

        if n < 0:
            f = '1 /' + f

    else:
        if x != np.floor(x):
            x = np.floor(x)

        f = np.uint64(1.0)
        for i in np.arange(np.absolute(n)):
            f *= (x - i)

        if n < 0:
            f = 1 / f

    return f

def risingfactorial(x, n):
    r"""
    Computes the rising factorial. Also known as the Pochhammer symbol.

    Parameters
    ----------
    x
        Integer. The value will be rounded down if the value is not an integer.
    n
        Integer. The value will be rounded down if the value is not an integer.

    Returns
    -------
    int or str
        The rising factorial for an integer :math:`n`, :math:`(x)_{n}`. If x is
        a str, the output is the symbolic representation of the rising factorial.

    Notes
    -----
    The rising factorial, :math:`x^{(n)}` (sometimes denoted \langle x \rangle_n ) is
    also known as the Pochhammer symbol in other areas of mathematics.

    The rising factorial is related to the gamma function :math:`\Gamma (z)`.

    .. math::

        x^{(n)} \equiv \frac{\Gamma (x + n)}{\Gamma (n)}

    where :math:`x^(0) = 1`.

    The rising factorial is related to the falling factorial by:

    .. math::

        x^{(n)} = (-x)_n (-1)^n

    Examples
    --------
    >>> risingfactorial(10, 6)
    3603600
    >>> risingfactorial('x', 4)
    'x*(x + 1)*(x + 2)*(x + 3)'

    References
    ----------
    Falling and rising factorials. (2017, June 8). In Wikipedia, The Free Encyclopedia.
        From https://en.wikipedia.org/w/index.php?title=Falling_and_rising_factorials&oldid=784512036

    Weisstein, Eric W. "Rising Factorial." From MathWorld--A Wolfram Web Resource.
        http://mathworld.wolfram.com/RisingFactorial.html

    """
    if n != np.floor(n):
        n = np.floor(n)

    if isinstance(x, str):
        f = x

        for i in np.arange(1, np.absolute(n)):
            f = f + '*(' + str(x) + ' + ' + str(i) + ')'

        if n < 0:
            f = '1 /' + f

    else:
        if x != np.floor(x):
            x = np.floor(x)

        f = np.uint64(1.0)

        for i in np.arange(np.absolute(n)):
            f *= (x + i)

        if n < 0:
            f = 1 / f

    return f


import numpy as np
import matplotlib.pyplot as plt

def LR_1D_calculator(X,Y, xlabel=None, ylabel=None):
    # We now solve the equation y = aX + b
    X_mean = X.mean()
    X_sum = X.sum()
    Y_mean = Y.mean()
    Y_sum = Y.sum()

    denominator = X.dot(X) - X_mean * X_sum
    a = ( X.dot(Y) - Y_mean * X_sum) / denominator
    b = ( Y_mean * X.dot(X) - X_mean * X.dot(Y)) / denominator

    Y_hat = a * X + b

    plt.figure()
    plt.scatter(X,Y)
    plt.plot(X, Y_hat, 'r')
    if xlabel is not None:
        plt.xlabel(xlabel)
    if ylabel is not None:
        plt.ylabel(ylabel)
    plt.show()

    R2 = r2_calculator(Y, Y_hat)

    return Y_hat, a, b, R2

def r2_calculator(Y, Y_hat):

    # We also calculate the R^2 error:

    d1 = Y - Y_hat
    d2 = Y - Y.mean()
    R2 = 1 - d1.dot(d1) / d2.dot(d2)
    print("The R^2 is: ", R2)

    return R2

if __name__ == '__main__':

    # We first load and visualise the data:

    X = []
    Y = []
    for line in open('../large_files/data_1d.csv'):
        x, y = line.split(',')
        X.append(float(x))
        Y.append(float(y))

    X = np.array(X)
    Y = np.array(Y)

    plt.figure()
    plt.scatter(X,Y)
    plt.show()

    LR_1D_calculator(X,Y)


# add() is done between two elements : return an array
# sum([]) happens over n elements : return an element

import numpy as np

arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])

newarr = np.add(arr1, arr2)

print("add():", newarr)

newarr = np.sum([arr1, arr2])
# note that sum([])

print("sum(): ", newarr)

# Summation over an axis

newarr = np.sum([arr1, arr2], axis = 1)

print("sum() over an axis", newarr)

# cummulative sum : partially adding the elements in array

# The partial sum of [1, 2, 3, 4] would be [1, 1+2, 1+2+3, 1+2+3+4] = [1, 3, 6, 10]

# cumsum()

# import numpy as np

arr = np.array([1, 2, 3])

print("cummulative Sum: ", np.cumsum(arr))


import scipy.sparse.linalg
import scipy.sparse as sp
import matplotlib.pylab as plt
import numpy as np

N = 100
e = np.ones(N, dtype=float)
A = sp.spdiags([e, -2*e, e], [-1, 0, 1], N, N, format='csc')

plt.spy(A)
plt.show()

L = 2*np.pi
x = np.linspace(0, L, N+2)
h = x[1] - x[0]
fcos = 2 * np.cos(x) / np.exp(x)
analytical_cos = -np.sin(x) / np.exp(x)
fsin = 2 * np.sin(x) / np.exp(x)
analytical_sin = np.cos(x) / np.exp(x)

# Use sparse lu decomposition, matrix is tridiagonal so no fill-in
splu = sp.linalg.splu(A / h**2)
fig, axs = plt.subplots(1, 2)
axs[0].spy(splu.L)
axs[0].set_title('L')
axs[1].spy(splu.U)
axs[1].set_title('U')
plt.show()

# Use decomposition to solve both problems
for f, a in zip([fcos, fsin], [analytical_cos, analytical_sin]):
    b = f[1:-1]
    b[0] -= a[0] / h**2
    b[-1] -= a[-1] / h**2
    v = splu.solve(b)
    plt.plot(x[1:-1], v, label='solution')
    plt.plot(x, a, label='analytical')
    plt.legend()
    plt.show()

import time
num = 100
times = np.empty(num, dtype=float)
times_dense = np.empty(num, dtype=float)
times_dense[:] = np.nan
Ns = np.logspace(0.5, 6, num=num, dtype=int)
for i, N in enumerate(Ns):
    e = np.ones(N, dtype=float)
    A = sp.spdiags([e, -2*e, e], [-1, 0, 1], N, N, format='csc')

    t0 = time.perf_counter()
    AA = A @ A
    t1 = time.perf_counter()
    times[i] = t1 - t0

    if N < 2000:
        Adense = A.toarray()
        t0 = time.perf_counter()
        AA = Adense @ Adense
        t1 = time.perf_counter()
        times_dense[i] = t1 - t0

plt.clf()
plt.loglog(Ns, times, label='sparse @')
plt.loglog(Ns, times_dense, label='dense @')
plt.xlabel('N')
plt.ylabel('time taken')
plt.legend()
plt.show()

times = np.empty(num, dtype=float)
times_dense = np.empty(num, dtype=float)
times_dense[:] = np.nan

for i, N in enumerate(Ns):
    e = np.ones(N, dtype=float)
    A = sp.spdiags([e, -2*e, e], [-1, 0, 1], N, N, format='csc')

    x = np.linspace(0, L, N+2)
    h = x[1] - x[0]
    fcos = 2 * np.cos(x) / np.exp(x)
    analytical_cos = -np.sin(x) / np.exp(x)

    A /= h**2

    b = fcos[1:-1]
    b[0] -= analytical_cos[0] / h**2
    b[-1] -= analytical_cos[-1] / h**2

    t0 = time.perf_counter()
    splu = sp.linalg.splu(A)
    v = splu.solve(b)
    t1 = time.perf_counter()
    times[i] = t1 - t0
    if N < 2000:
        Adense = A.toarray()
        t0 = time.perf_counter()
        lu = scipy.linalg.lu_factor(Adense)
        v = scipy.linalg.lu_solve(lu, b)
        t1 = time.perf_counter()
        times_dense[i] = t1 - t0

plt.clf()
plt.loglog(Ns, times, label='sparse LU')
plt.loglog(Ns, times_dense, label='dense LU')
plt.xlabel('N')
plt.ylabel('time taken')
plt.legend()
plt.show()


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

###
# Name: Trevor Kling
# Student ID: 002270716
# Email: kling109@mail.chapman.edu
# Course: PHYS220/MATH220/CPSC220 Fall 2018
# Assignment: CW08
###

import numpy as np

def s(T, t, n):
    """
    Implements a summation of a sin-based function with defined constants T and t.  The sum is computed
    up to a value n, and should converge to 1, -1, or 0 depending on the range t falls into.
    0 < t < T/2: Converges to 1.
    t = 0: Converges to 0.
    -T/2 < t < 0: Converges to -1
    """
    summationArray = np.arange(1, n+1)
    def func(k):
        return (((1)/(2*k - 1))*np.sin((2*(2*k - 1)*np.pi*t)/(T)))
    summer = np.vectorize(func)
    result = (4/np.pi)*np.sum(summer(summationArray))
    return result

def f(T, t):
    """
    Checks if t is between particular values, and returns what the above function will converge to at
    infinity for the given t value.
    """
    if 0 < t < (T/2):
        return 1
    elif t == 0:
        return 0
    elif -(T/2) < t < 0:
        return -1
    else:
        print("That is not within the given range of [-T/2, T/2]")
        return None


import numpy as np

temp_na = np.array([1, 2, 3, 4, 5, 6])
temp_na

temp_na.shape

# 基本统计
temp_na.max()
temp_na.min()
temp_na.mean()
temp_na.sum()

# np reshape 改变矩阵形状
temp_na = temp_na.reshape(2, 3)
temp_na

# tensor 切片
# 所有行的前面两列
temp_na[:, :2]

# 矩阵计算
np.random([3, 5])
na1 = np.random.rand(2, 3)
na2 = np.random.rand(3, 5)
na1
na2
np.dot(na1, na2)

# 创建各种矩阵
zeroarray = np.zeros((2, 3))
print(zeroarray)

onearray = np.ones((3, 4), dtype='int64')
print(onearray)

emptyarray = np.empty((3, 4))
print(emptyarray)

array = np.arange(10, 31, 5)
print(array)

# 矩阵的na的属性
array = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
print(array)
# 数组维度
print(array.ndim)
# 数组形状
print(array.shape)
# 数组元素个数
print(array.size)
# 数组元素类型
print(array.dtype)

# 改变na的形状
array1 = np.arange(6).reshape([2, 3])
print(array1)

array2 = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int64).reshape([3, 2])
print(array2)

# 矩阵的计算
arr1 = np.array([[1, 2, 3], [4, 5, 6]])
arr2 = np.ones([2, 3], dtype=np.int64)
print(arr1)
print(arr2)

# 矩阵的基本运算
print(arr1 + arr2)
print(arr1 - arr2)
print(arr1 * arr2)
print(arr1 / arr2)
# 平方
print(arr1 ** 2)

# 矩阵的乘法
arr3 = np.array([[1, 2, 3], [4, 5, 6]])
arr4 = np.ones([3, 2], dtype=np.int64)
print(arr3)
print(arr4)
print(np.dot(arr3, arr4))

# np的其他统计分析函数
print(arr3)
print(np.sum(arr3, axis=1))  # axis=1,每一行求和 axie=0,每一列求和
print(np.max(arr3))
print(np.min(arr3))
print(np.mean(arr3))
print(np.argmax(arr3))
print(np.argmin(arr3))

# 矩阵的转置
arr3_tran = arr3.transpose()
print(arr3_tran)
print(arr3.flatten())

# 矩阵的索引和切片
arr5 = np.arange(0, 6).reshape([2, 3])
print(arr5)
print(arr5[1])
print(arr5[1][2])
print(arr5[1, 2])
print(arr5[1, :])
print(arr5[:, 1])
print(arr5[1, 0:2])


import copy
import numpy as np
import numpy.linalg as la
from scipy.optimize import linprog  # TODO: REMOVE

from _errors import ConvergenceError

# ======================================================================================================================
# Root-finding Methods
# ======================================================================================================================
def secant(fun, x0, x1, args=()):
    # options ----------------------------------------------------------------------------------------------------------
    max_it = 1000
    tol = 1e-3

    # initializing loop ------------------------------------------------------------------------------------------------
    it = 0
    root = x1

    # iterating --------------------------------------------------------------------------------------------------------
    while abs(x1 - x0) > tol and it < max_it:
        f0 = fun(x0, *args)
        f1 = fun(x1, *args)
        root -= f1 * (root - x0) / (f1 - f0)

        if root in (np.inf, np.nan):
            raise ConvergenceError('division by zero')

        x0 = x1
        x1 = root
        it += 1

    return root

# ======================================================================================================================
# Least Squares Methods
# ======================================================================================================================
def residual(f, x, y, p, args=()):
    return y - f(x, *p, *args)

def lsq_obj(r):
    return 0.5 * la.norm(r) ** 2.

def d_lsq_obj(r, j):
    return j.T @ r

def jacobian_fd(x, p, f, args=()):
    m = len(p)
    j = [None for _ in range(0, m)]

    eps = 1e-8
    fx = f(x, *p, *args)

    for i in range(0, m):
        p_ = copy.deepcopy(list(p))
        p_[i] += eps
        j[i] = (f(x, *p_, *args) - fx) / eps

    return np.asarray(j).T

def nl_lsq(fun, x, y, p0, jac=None, args=()):
    # options ----------------------------------------------------------------------------------------------------------
    max_it = 1000
    max_it_bt = 100
    tol = 1e-3
    rho = 0.5
    c = 1e-4

    if jac is None:
        jac = lambda xj, *pj: jacobian_fd(xj, pj, fun, args=args)

    # initializing loop ------------------------------------------------------------------------------------------------
    it = 0
    converged = False
    p = p0
    res = residual(fun, x, y, p, args=args)
    j = jac(x, *p, *args)
    f = lsq_obj(res)
    df = d_lsq_obj(res, j)

    # iterating --------------------------------------------------------------------------------------------------------
    while not converged and it < max_it:
        # calculate optimized step
        try:

            q, r = la.qr(j)
            dp = la.solve(r, q.T @ res)

        except np.linalg.LinAlgError:
            raise ConvergenceError('Unable to find a solution due to singular matrix issues')

        # invoke backtracking
        alpha = 1.
        it_bt = 0

        p_bt = p + dp
        f_bt = lsq_obj(residual(fun, x, y, p_bt, args=args))
        csdf = -c * np.dot(dp, df)

        while f_bt >= (f + alpha * csdf) and it_bt < max_it_bt:
            p_bt = p + alpha * dp
            f_bt = lsq_obj(residual(fun, x, y, p_bt, args=args))

            alpha *= rho
            it_bt += 1

        p = p_bt

        # update parameters and check convergence
        res = residual(fun, x, y, p, args=args)
        f = lsq_obj(res)
        j = jac(x, *p_bt, *args)
        df = d_lsq_obj(res, j)

        if la.norm(df, np.inf) < tol:
            converged = True

        it += 1

    if it == max_it:
        raise ConvergenceError('Solver failed to converge within maximum number of iterations')

    return p

# ======================================================================================================================
# Linear Programming Methods
# ======================================================================================================================
def lin_ip(A, g, b):
    """
    TODO: NOT WORKING
    # Algorithm 14.3, Page 411 Nocedal & Wright

    Parameters
    ----------
    A : array_like
        system matrix of the constraints
    g : array_like
        objective function multiplier
    b : array_like
        right-hand side of the constrains

    Returns
    -------
    array_like
        optimal solution vector
    """

    converged = False
    m, n = A.shape
    max_iter = 10
    iter_ = 0
    eta = 0.99

    # initial value correction heuristic -------------------------------------------------------------------------------
    AA = A @ A.T
    x_t = A.T @ la.solve(AA, b)
    l_t = la.solve(AA, A @ g)
    s_t = g - A.T @ l_t

    dx = max(-1.5 * x_t.min(), 0.)
    ds = max(-1.5 * s_t.min(), 0.)
    x_h = x_t + dx
    s_h = s_t + ds

    xhsh = x_h.T @ s_h
    dx_h = .5 * xhsh / (np.sum(s_h))
    ds_h = .5 * xhsh / (np.sum(x_h))

    x = x_h + dx_h
    l = l_t
    s = s_h + ds_h

    # main loop --------------------------------------------------------------------------------------------------------
    r_c = A.T @ l + s - g
    r_b = A @ x - b
    mu = (x.T @ s) / n

    while (not converged) and (iter_ < max_iter):
        iter_ = iter_ + 1

        # KKT system
        kkt = np.block([[np.zeros((n, n)), A.T, np.eye(n)],
                        [A, np.zeros((m, m)), np.zeros((m, n))],
                        [np.diag(s.flatten()), np.zeros((n, m)), np.diag(x.flatten())]])

        rhs = np.vstack((-r_c, -r_b, -x * s))

        # Solving for and extracting affine variables
        # QR decompose KKT matrix, TODO: LDL decomposition instead
        q, r = la.qr(kkt)
        dv_aff = q @ la.solve(r.T, rhs)

        dx_aff = dv_aff[:n]
        ds_aff = dv_aff[(n + m):]

        # Determining indices and corresponding alpha for affine variables
        alpha_prim_aff = np.where(dx_aff < 0., -x / dx_aff, 1.).min()
        alpha_dual_aff = np.where(ds_aff < 0., -s / ds_aff, 1.).min()

        # Calculating affine mu, mu and sigma
        mu_aff = ((x + alpha_prim_aff * dx_aff).T @ (s + alpha_dual_aff * ds_aff)) / n
        sigma = (mu_aff / mu) ** 3. if mu > 1.e-10 else 0.

        rhs = np.vstack((-r_c, -r_b, -x * s - dx_aff * ds_aff + sigma * mu))

        # Solving for and extracting increments
        dv = q @ la.solve(r.T, rhs)
        dx = dv[:n]
        dl = dv[n:(n + m)]
        ds = dv[(n + m):]

        # Determining indices and corresponding alpha for x and s
        alpha_prim = np.where(dx < 0., eta * (-x / dx), 1.).min()
        alpha_dual = np.where(ds < 0., eta * (-s / ds), 1.).min()

        # updating x, l and s
        x += alpha_prim * dx
        l += alpha_dual * dl
        s += alpha_dual * ds

        print('X')
        print(x)

        # convergence check
        r_c = A.T @ l + s - g
        r_b = A @ x - b
        mu = (x.T @ s) / n

        converged = (la.norm(r_c, ord=np.inf) <= 1.e-9) and (la.norm(r_b, ord=np.inf) <= 1.e-9) and (abs(mu) <= 1.e-9)
        print('CONVERGENCE')
        print('rC', la.norm(r_c, ord=np.inf))
        print('rA', la.norm(r_b, ord=np.inf))
        print('mu', abs(mu))

    return x

# ======================================================================================================================
# Quadratic Programming Methods
# ======================================================================================================================
def nl_sqp(obj, con, x0, H0):
    """
    Non-linear SQP solver for inequality constrained problems
    TODO: Implement equality constraints
    :param obj:
    :param con:
    :param x0:
    :param H0:
    :return:
    """
    # Options ----------------------------------------------------------------------------------------------------------
    tol = 1.0e-3
    max_iter = 300
    n = x0.shape[0]

    # calculating objective function and constraint function using a numerical approximation for Jacobians
    xeval = x0
    f, df = obj(xeval)
    c, dc = con(xeval)

    m = c.size
    mu = 100.

    # assembling KKT system
    A = np.zeros((n + m, 0))  # incorrect, assemble for equality constraints
    b = np.zeros(0)  # incorrect, assemble for equality constraints
    H = np.block([[np.zeros(H0.shape), np.zeros((H0.shape[0], m))], [np.zeros((m, H0.shape[1])),  np.eye(m) * 1e-6]])
    g = np.block([np.zeros((df.shape[0], 1)), np.zeros((m, 1))])
    y = np.zeros(0)

    C = np.block([[np.zeros(dc.shape), np.zeros((m, m))], [np.zeros((m, n)), np.eye(m)]])
    d = np.zeros(2 * m)
    B = H0

    z = np.abs(la.solve(dc, df))
    s = np.ones(2 * m)
    dLold = df - dc @ z

    # Main loop iterations ---------------------------------------------------------------------------------------------
    converged = (la.norm(dLold, ord=np.inf) < tol) and (la.norm(z * c, ord=np.inf) < tol)  # z * c element wise

    rho = 0.5
    iter = 0

    while (not converged) and (iter < max_iter):
        # Updating initial guess input for the PDPCIP algorithm
        H[:n, :n] = B
        g = np.block([df, mu * np.ones(m)])
        # TODO: Missing the equality constrains here?
        C[:m, :m] = dc
        d[:m] = -c

        zpad = np.block([z, np.ones(m)])
        t = np.maximum(-(c + dc @ xeval), np.zeros(m))
        xt = np.block([xeval, t])

        # Sub problem: Solve constrained QP
        p, y, z, _ = quad_ip(H, g, A, b, C, d, xt, y, s, zpad)

        xeval = xt[:n]
        z = z[:n]
        p = p[:n]

        # Take step
        xeval += p

        # Function evaluation
        f, df = obj(xeval)
        c, dc = con(xeval)
        mu = (df.T @ p + 0.5 * p.T @ B @ p) / ((1. - rho) * la.norm(c, ord=1))

        # Lagrangian gradient, z used for inequality constraints
        dLnew = df - dc @ z

        # BFGS Hessian update
        q = dLnew - dLold
        Bp = B @ p

        if np.dot(p, q) >= 0.2 * np.dot(p, Bp):
            theta = 1.
        else:
            theta = (0.8 * np.dot(p, Bp)) / (np.dot(p, Bp) - np.dot(p, q))

        r = theta * q + (1. - theta) * Bp
        r = r.reshape((r.shape[0], 1))
        Bp = Bp.reshape((Bp.shape[0], 1))
        B += r @ r.T / np.dot(p, r) - Bp @ Bp.T / np.dot(p, Bp)

        dLold = dLnew
        iter += 1
        converged = (la.norm(dLold, np.inf) < tol) and (la.norm(z * c, np.inf) < tol)  # z * c piecewise

    info = converged
    zopt = z[:2]
    xopt = xeval
    return xopt, zopt, info

def quad_ip(H, g, A, b, C, d, x0, y0, s0, z0):
    """
    Primal Dual Predictor Corrector Interior Point Algorithm.
    :param H:
    :param g:
    :param A:
    :param b:
    :param C:
    :param d:
    :param x0:
    :param y0:
    :param s0:
    :param z0:
    :return:
    """
    # Options ----------------------------------------------------------------------------------------------------------
    epsilon = 1e-3
    max_iter = 100

    # Heuristic for initial point --------------------------------------------------------------------------------------
    mc = z0.size
    KKT0 = np.zeros((A.shape[1], A.shape[1]))

    rL = H @ x0 + g - A @ y0 - C @ z0
    rA = b - A.T @ x0
    rC = s0 + d - C.T @ x0
    rsz = s0 * z0  # element-wise

    # assemble KKT matrix
    H0 = H + (C @ np.diag((z0 / s0)) @ C.T)  # z0 / s0 element wise
    KKT = np.block([[H0, -A], [-A.T, KKT0]])

    # assemble RHS of KKT system
    rL0 = rL - C @ np.diag((z0 / s0)) @ (rC - rsz / z0)  # z0 / s0 and rsz. / z0 elementwise
    RHS = -np.block([rL0, rA])

    # QR decompose KKT matrix, TODO: LDL decomposition instead
    Q, R = la.qr(KKT)
    affvec = Q @ la.solve(R.T, RHS)

    xaff = affvec[:x0.shape[0]].T
    zaff = -np.diag((z0 / s0)) @ C.T @ xaff + np.diag((z0 / s0)) @ (rC - rsz / z0)  # z0 / s0 and rsz / z0 element wise
    saff = -rsz / z0 - np.diag((s0 / z0)) @ zaff  # rsz / z0 and s0 / z0 element wise element wise

    x = x0
    y = y0
    z = np.maximum(np.ones(z0.shape), abs(z0 + zaff))
    s = np.maximum(np.ones(s0.shape), abs(s0 + saff))
    mu0 = np.dot(z, s) / mc
    zdivs = z / s  # element wise

    # Iterations -------------------------------------------------------------------------------------------------------
    rL = H @ x + g - A @ y - C @ z
    rA = b - A.T @ x
    rC = s + d - C.T @ x
    rsz = s * z  # element wise
    mu = mu0

    iter = 0
    converged = convergence_check(rL, rA, rC, mu0, mu0, H, g, A, b, C, d, epsilon)

    while (not converged) and (iter < max_iter):
        # assemble KKT matrix
        Hbar = H + (C @ np.diag(zdivs) @ C.T)
        KKT = np.block([[Hbar, -A], [-A.T, KKT0]])

        # assemble RHS of KKT system
        rLbar = rL - C @ np.diag(zdivs) @ (rC - rsz / z)  # rsz / z element wise
        RHS = np.block([-rLbar, -rA])

        # QR decompose KKT matrix, TODO: LDL decomposition instead
        # [L, D, p] = ldl(KKT, 'lower', 'vector')
        # affvec = zeros(1, numel(RHS))
        # affvec(p) = L'\(D\(L\RHS(p)))
        Q, R = la.qr(KKT)  # may need method='complete
        affvec = Q @ la.solve(R.T, RHS)

        xaff = affvec[:x.shape[0]].T
        zaff = - np.diag(zdivs) @ C.T @ xaff + np.diag(zdivs) @ (rC - rsz / z)  # rsz / z element wise
        saff = -(rsz / z) - np.diag(s / z) @ zaff  # rsz / z and s / z element wise

        alphaaff1 = np.where(zaff < 0., -z / zaff, 1.).min()  # z / zaff element wise
        alphaaff2 = np.where(saff < 0., -s / saff, 1.).min()  # s / saff element wise
        alphaaff = np.minimum(alphaaff1, alphaaff2)

        muaff = np.dot((z + alphaaff * zaff), (s + alphaaff * saff)) / mc
        sigma = (muaff / mu) ** 3.

        # assembling updated RHS of KKT system
        rszbar = rsz + saff * zaff - sigma * mu  # saff * zaff element wise
        rLbar = rL - C @ np.diag(zdivs) @ (rC - rszbar / z)  # rszbar / z element wise
        RHS = np.block([-rLbar, -rA])

        # TODO: LDL decomposition
        # vec = np.zeros(numel(RHS), 1)
        # vec(p) = L'\(D\(L\RHS(p)))
        vec = Q @ la.solve(R.T, RHS)

        # calculate step size
        dx = vec[:x.shape[0]]
        dy = vec[x.shape[0] + 1:]
        dz = -np.diag(zdivs) @ C.T  @ dx + np.diag(zdivs) @ (rC-rszbar / z)  # rszbar / z element wise
        ds = -rszbar / z - np.diag(s / z) @ dz  # rszbar / z and s / z element wise

        alpha1 = np.where(dz < 0., -z / dz, 1.).min()  # z / dz element wise
        alpha2 = np.where(ds < 0., -s / ds, 1.).min()  # s / ds element wise
        alpha = np.minimum(alpha1, alpha2)

        # updating estimator with calculated step-size
        alphabar = 0.995 * alpha
        x = x + dx * alphabar
        y = y + dy * alphabar
        z = z + dz * alphabar
        s = s + ds * alphabar

        rL = H @ x + g - A @ y - C @ z
        rA = b - A.T @ x
        rC = s + d - C.T @ x
        rsz = s * z  # element wise
        mu = np.dot(z, s) / mc
        zdivs = z / s  # element wise
        converged = convergence_check(rL, rA, rC, mu, mu0, H, g, A, b, C, d, epsilon)
        iter += 1

    if converged:
        xopt = x
        yopt = y
        zopt = z
        sopt = s
    else:
        xopt = []
        yopt = []
        zopt = []
        sopt = []

    return xopt, yopt, zopt, sopt

def convergence_check(rL, rA, rC, mu, mu0, H, g, A, b, C, d, tol):
    conv = False
    rAcheck = True
    rCcheck = True

    # KKT system
    kkt = np.block([H, g.reshape((g.shape[0], 1)), A, C])
    rLcheck = la.norm(rL, ord=np.inf) <= tol * np.maximum(1., la.norm(kkt, ord=np.inf))

    # equality constraints
    eq = np.block([A.T, b.reshape((b.shape[0], 1))])
    if eq.size:
        rAcheck = la.norm(rA, ord=np.inf) <= tol * np.maximum(1., la.norm(eq, ord=np.inf))

    # inequality constraints
    ineq = np.block([np.eye(d.size), d.reshape((d.shape[0], 1)), C.T])
    if ineq.size:
        rCcheck = la.norm(rC, ord=np.inf) <= tol * np.maximum(1., la.norm(ineq, ord=np.inf))

    muCheck = np.abs(mu) <= tol * 1e-2 * mu0

    if rLcheck and rAcheck and rCcheck and muCheck:
        conv = True

    return conv

# QUADRATIC TEST SCRIPT ------------------------------------------------------------------------------------------------
#def func(x, a, b, c):
#    return a * np.exp(-b * x) + c

#def func_jac(x, a, b, c):
#    return np.array([np.exp(-b * x), -a * x * np.exp(-b * x), np.repeat(1., x.size)]).T

#xdata = np.linspace(0, 4, 50)
#y = func(xdata, 2.5, 1.3, 0.5)
#np.random.seed(1729)
#y_noise = 0.2 * np.random.normal(size=xdata.size)
#ydata = y + y_noise
#p = nl_lsq(func, xdata, ydata, p0=np.asarray([1., 1., 1.]), jac=func_jac)

# LINEAR TEST SCRIPT ---------------------------------------------------------------------------------------------------
# n = 2                     # Number of variables and constraints (2 for contour plot)
# A = np.random.rand(n, n)  # Generating A
# k = 2                     # Index to put in zeros
#
# xp = abs(np.random.rand(n, 1))
# xp[k:] = np.zeros((xp.size-k, 1))
# sp = abs(np.random.rand(n, 1))
# sp[:k-1] = np.zeros((sp.size-k + 1, 1))
#
# lp = np.random.rand(n, 1)   # Generating lambda
#
# g = A.T @ lp + sp      # Computing g
# b = A @ xp             # Computing b

# problem from: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.linprog.html
# A = np.zeros((2, 2))
# A[0, :] = np.array([-3., 1.])
# A[1, :] = np.array([1., 2.])
#
#
# b = np.zeros((2, 1))
# b[0, 0] = 6.
# b[1, 0] = 4.
#
# g = np.zeros((2, 1))
# g[0, 0] = -1.
# g[1, 0] = 4.
#
# # Solving problem using Linear Programming Predictor-Correction IP algorithm
# x_opt = lin_ip(A, g, b)
# #print(x_opt)
# obj = g.T @ x_opt
# #print(obj)
# print('OPTIMUM')
# print(x_opt, obj)
#
# c = [-1, 4]
# A = [[-3, 1], [1, 2]]
# b = [6, 4]
# #x0_bounds = (None, None)
# #x1_bounds = (-3, None)
# res = linprog(c, A_ub=A, b_ub=b)#, bounds=[x0_bounds, x1_bounds])
# print(res)


# --------------
#Importing header files
import pandas as pd
import scipy.stats as stats
import math
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.stats.weightstats import ztest
from statsmodels.stats.weightstats import ztest
from scipy.stats import chi2_contingency

import warnings

warnings.filterwarnings('ignore')
#Sample_Size
sample_size=2000

#Z_Critical Score
z_critical = stats.norm.ppf(q = 0.95)
print('z_critical:', z_critical)
# Critical Value
critical_value = stats.chi2.ppf(q = 0.95, # Find the critical value for 95% confidence*
                      df = 6)   # Df = number of variable categories(in purpose) - 1
print('critical_value', critical_value)

#Reading file
data=pd.read_csv(path)
data_sample=data.sample(n=2000, random_state=0)
sample_mean=data_sample.installment.mean()
print('sample mean:', sample_mean)

sample_std=data_sample.installment.std()
print('sample_std:', sample_std)

margin_of_error = z_critical * (sample_std/math.sqrt(sample_size))
print("Margin of error:",margin_of_error)

confidence_interval = (sample_mean - margin_of_error, sample_mean + margin_of_error)
print("confidence interval:", confidence_interval)

true_mean = data.installment.mean()
print("True mean of data:", true_mean)

# CLT
sample_size = np.array([20,50,100])
fig, axes = plt.subplots(3,1, figsize=(10,20))

for i in range(len(sample_size)):
    m = []
    for j in range(1000):
        mean = data['installment'].sample(sample_size[i]).mean()
        m.append(mean)
    mean_series = pd.Series(m)
    axes[i].hist(mean_series)
plt.show()

data['int.rate'] = data['int.rate'].map(lambda x: str(x)[:-1])
data['int.rate'] = data['int.rate'].astype(float)/100

z_statistic_1, p_value_1 = ztest(x1 = data[data['purpose'] == 'small_business']['int.rate'], value = data['int.rate'].mean(), alternative = 'larger')

print("z-statistic is:", z_statistic_1)
print("p-value is:", p_value_1)

z_statistic_2, p_value_2 = ztest(x1 = data[data['paid.back.loan'] == 'No']['installment'], x2 = data[data['paid.back.loan'] == 'Yes']['installment'])

print("z-statistic 2 is:", z_statistic_2)
print("p-value 2 is:", p_value_2)

yes = data[data['paid.back.loan'] == 'Yes']['purpose'].value_counts()
no = data[data['paid.back.loan'] == 'No']['purpose'].value_counts()
print(yes)
print(no)
observed = pd.concat([yes.transpose(), no.transpose()], 1,keys=['Yes','No'])
print(observed)

chi2, p, dof, ex = chi2_contingency(observed)

print("Critical value is:", critical_value)

print("chi statistic is:", chi2)

#Code starts here

import numpy,import scipy,from scipy,from statsmodels


"""
Problem 12
The sequence of triangle numbers is generated by adding the natural numbers.
So the 7th triangle number would be 1 + 2 + 3 + 4 + 5 + 6 + 7 = 28.

The first ten terms would be:
1, 3, 6, 10, 15, 21, 28, 36, 45, 55, ...

Let us list the factors of the first seven triangle numbers:

     1: 1
     3: 1,3
     6: 1,2,3,6
    10: 1,2,5,10
    15: 1,3,5,15
    21: 1,3,7,21
    28: 1,2,4,7,14,28
We can see that 28 is the first triangle number to have over five divisors.

What is the value of the first triangle number to have over five hundred divisors?
"""
import numpy as np

def generate_triangle_numbers(n):
    # triangular numbers can be calculated with dynamic formula below
    # instead of summing all numbers from 1 till n...
    return int(n * (n + 1) / 2)

def number_of_divisors(n):
    factors = set()

    # a factor will always be a pair.
    # the lesser of the pairs can never be greater than sqrt(n)
    for i in range(1, int(np.sqrt(n) + 1)):
        if n % i == 0:  # finds the "lesser" factors
            factors.update([i])  # saves "lesser"
            factors.update([n // i])  # saves floor division of "lesser" - complement in the 'pair'
    return len(factors)

j = 1

while number_of_divisors(generate_triangle_numbers(j)) < 500:
    j += 1

print('winner winnner: {0}'.format(generate_triangle_numbers(j)))

Solutions/Problem 12 - Highly divisible triangular number.py


# Importing the libraries
import pandas as pd
import statistics

#import scipy as statistics

data1 = [1, 2, 3, 4, 4, 5, 7, 9]

data1_mean = statistics.mean(data1)

print("Mean is :", data1_mean)
print("Mean is : %.2f" % (data1_mean))
print("Mean is : %.3f" % (data1_mean))
print("Mean is : %.4f" % (data1_mean))
print("Mean is : %.5f" % (data1_mean))
print("Mean is : %.6f" % (data1_mean))

data1_median = statistics.median(data1)
print("Median is :", data1_mean)
data1_mode = statistics.mode(data1)
print("Mode is :", data1_mode)
data1_stdev = statistics.stdev(data1)
print("Stdev is :", data1_stdev)
data1_variance = statistics.variance(data1)
print("Variance is :", data1_variance)

# Importing the Salary .csv data set
salaryDS = pd.read_csv('Dummy_Salary_Data.csv')
print (salaryDS)

exp = salaryDS.iloc[:, 0].values
salary = salaryDS.iloc[:, 1].values

print ("exp: ", exp)

exp_mean = statistics.mean(exp)
exp_median = statistics.median(exp)
exp_mode = statistics.mode(exp)
exp_stdv = statistics.stdev(exp)
exp_variance = statistics.variance(exp)

print ("exp_mean: ", exp_mean)
print ("exp_median: ", exp_median)
print ("exp_mode: ", exp_mode)
print ("exp_stdv: ", exp_stdv)
print ("exp_variance: ", exp_variance)

print ("salary: ", salary)

salary_mean = statistics.mean(salary)
salary_median = statistics.median(salary)
salary_mode = statistics.mode(salary)
salary_stdv = statistics.stdev(salary)
salary_variance = statistics.variance(salary)

print ("salary_mean: ", salary_mean)
print ("salary_median: ", salary_median)
print ("salary_mode: ", salary_mode)
print ("salary_stdv: ", salary_stdv)
print ("salary_variance: ", salary_variance)

# Importing the Titanic .csv data file
titanicDS = pd.read_csv('Dummy_Titanic_Data.csv')
print (titanicDS)

age = titanicDS.iloc[:, 4].values

ticket_fare = gender = titanicDS.iloc[:, 7].values

print ("age: ", age)

age_mean = statistics.mean(age)
age_median = statistics.median(age)
age_mode = statistics.mode(age)
age_stdv = statistics.stdev(age)
age_variance = statistics.variance(age)

print ("age_mean: ", age_mean)
print ("age_median: ", age_median)
print ("age_mode: ", age_mode)
print ("age_stdv: ", age_stdv)
print ("age_variance: ", age_variance)

print ("ticket_fare: ", ticket_fare)

ticket_fare_mean = statistics.mean(ticket_fare)
ticket_fare_median = statistics.median(ticket_fare)
ticket_fare_mode = statistics.mode(ticket_fare)
ticket_fare_stdv = statistics.stdev(ticket_fare)
ticket_fare_variance = statistics.variance(ticket_fare)

print ("ticket_fare_mean: ", ticket_fare_mean)
print ("ticket_fare_median: ", ticket_fare_median)
print ("ticket_fare_mode: ", ticket_fare_mode)
print ("ticket_fare_stdv: ", ticket_fare_stdv)
print ("ticket_fare_variance: ", ticket_fare_variance)

Data Analysis/Some Basic and Useful Statistical Calculations with Python Anaconda Spyder.py


import scipy.stats
import numpy as np

def one_way_anova(*args):
    """One-way ANOVA to check if all groups have the same pop. mean.

    ANOVA stands for `Analysis of Variance`.

    Assumptions:
        i.i.d. x_{(1, 1)}, ..., x_{(1, m)} ~ N(mu_1, sigma^{2})
        i.i.d. x_{(2, 1)}, ..., x_{(2, m)} ~ N(mu_2, sigma^{2})
        ...
        i.i.d. x_{(n, 1)}, ..., x_{(n, m)} ~ N(mu_n, sigma^{2})

        Note 1: all groups (1 to n) has the same number of samples `m`.
        Note 2: all groups (1 to n) has the same pop. variance sigma^{2}.

    Test statistic: w = msb / msw
    where:
        msb = m * var(group_means);
        msw = mean(group_vars).

    Null distribution: W ~ F(n - 1, n * (m - 1)), where F is the F-distribution,
    `n` is the number of groups and `m` is the number of samples per group.

    H0: mu_1 = mu_2 = ... = mu_n
    HA: Exists at least a pair (i, j), 1 <= i, j <= n, such that mu_i != mu_j.
    """
    assert len(set(map(len, args))) == 1

    num_groups, num_inst_per_group = len(args), len(args[0])

    group_means = list(map(np.mean, args))
    group_vars = list(map(lambda arr: np.var(arr, ddof=1), args))

    msb = num_inst_per_group * np.var(group_means, ddof=1)
    msw = np.mean(group_vars)

    null_dist = scipy.stats.f(num_groups - 1, num_groups * (num_inst_per_group - 1))

    test_statistic = msb / msw
    p_value = null_dist.sf(test_statistic)

    return test_statistic, p_value

def f_test_for_equal_means(*args):
    """Also known as One-way ANOVA."""
    return one_way_anova(*args)

def _test():
    for n_groups in (2, 5, 10, 20):
        samples = [np.random.randn(50) for _ in range(n_groups)]
        res = one_way_anova(*samples)
        print(res)
        assert np.allclose(res, scipy.stats.f_oneway(*samples))

        samples[1] += 3 * np.random.random() - 1.5
        res = one_way_anova(*samples)
        print(res)
        assert np.allclose(res, scipy.stats.f_oneway(*samples))

if __name__ == "__main__":
    _test()


from typing import Tuple

import numpy as np

from utils.common_types import *

def wedge_mask(img_dim: Tuple[int, int], wedge_width_ratio: float = 0.2) -> NumpyArray:

    img_dimy, img_dimx = img_dim

    xx, yy = np.meshgrid(range(img_dimx), range(img_dimy))

    xx = xx / img_dimx
    yy = yy / img_dimy

    xx = xx - 1/2
    yy = yy - 1/2

    mask = np.logical_or(
        np.abs(xx) < wedge_width_ratio/2, np.abs(yy) < wedge_width_ratio/2
    )

    return mask[None, :, :]

def circle_mask(img_dim: Tuple[int, int], diameter_ratio: float = 0.5) -> NumpyArray:

    img_dimy, img_dimx = img_dim

    xx, yy = np.meshgrid(range(img_dimx), range(img_dimy))

    xx = xx / img_dimx
    yy = yy / img_dimy

    xx = xx - 1/2
    yy = yy - 1/2

    mask = xx**2 + yy**2 < (diameter_ratio/2) ** 2

    return mask[None, :, :]

def circular_strip_mask(
    img_dim: Tuple[int, int],
    outer_diameter_ratio: float = 0.2, inner_diameter_ratio: float = 0.1
) -> NumpyArray:

    outer_circle = circle_mask(img_dim, diameter_ratio=outer_diameter_ratio)
    inner_circle = circle_mask(img_dim, diameter_ratio=inner_diameter_ratio)

    strip = np.logical_xor(outer_circle, inner_circle)

    return strip

def rectangle_mask(
    img_dim: Tuple[int, int], height_ratio: float = 0.3, width_ratio: float = 0.7
) -> NumpyArray:

    img_dimx, img_dimy = img_dim

    xx, yy = np.meshgrid(range(img_dimx), range(img_dimy))

    xx = xx / img_dimx
    yy = yy / img_dimy

    xx = xx - 1/2
    yy = yy - 1/2

    mask = np.logical_and(
        np.abs(xx) < width_ratio/2, np.abs(yy) < height_ratio/2
    )

    return mask[None, :, :]

def square_mask(img_dim: Tuple[int, int], width_ratio: float = 0.5) -> NumpyArray:
    return rectangle_mask(img_dim, width_ratio, width_ratio)

def apply_mask(arr: NumpyArray, mask: NumpyArray) -> NumpyArray:

    channels = arr.shape[-3]

    if channels != 1:
        mask = np.concatenate([mask] * channels, axis=0)

    if len(arr.shape) == 3:
        return arr[mask]

    if len(arr.shape) == 4:
        return arr[:, mask]

    assert False


import numpy as np

class PCA:
    def __init__(self, threshold: float, n_components: int=None):
        if threshold == None:
            self.mode = 'number'
            self.n_components = n_components
        else:
            self.mode = 'threshold'
            self.threshold = threshold

    def fit(self, X: np.array):
        n_samples, n_dimensions = X.shape
        self.mean_vector = X.mean(0)
        self.A = X - self.mean_vector
        Sigma = np.cov(self.A, rowvar=False)
        eigen_values, eigen_vectors = np.linalg.eig(Sigma)
        indices = np.argsort(eigen_values)[::-1]
        eigen_values = eigen_values[indices]
        eigen_vectors = eigen_vectors[:, indices]

        explained_variance_ratio = eigen_values / eigen_values.sum()
        if self.mode == 'threshold':
            cumulative_expl_var = np.cumsum(explained_variance_ratio)
            self.n_components = np.where(cumulative_expl_var > self.threshold)[0][0]
        self.explained_variance_ratio = explained_variance_ratio[:self.n_components]
        self.eigen_values = eigen_values[:self.n_components]
        self.eigen_vectors = eigen_vectors[:, :self.n_components]

    def transform(self) -> np.array:
        return self.A @ self.eigen_vectors

    def fit_transform(self, X: np.array) -> np.array:
        self.fit(X)
        return self.transform()

    def reconstruct(self, X_hat) -> np.array:
        return (X_hat @ self.eigen_vectors.T) + self.mean_vector


from __future__ import print_function
from scipy.spatial.distance import cdist
import numpy as np
import matplotlib.pyplot as plt

np.random.seed(11)

means = [[2, 2], [8, 3], [3, 6]]
cov = [[1, 0], [0, 1]]
N = 500

X0 = np.random.multivariate_normal(means[0], cov, N)
X1 = np.random.multivariate_normal(means[1], cov, N)
X2 = np.random.multivariate_normal(means[2], cov, N)

X = np.concatenate((X0, X1, X2), axis = 0)
K = 3

original_label = np.asarray([0]*N + [1]*N + [2]*N).T

def kmeans_display(X, label):
    K = np.amax(label) + 1
    X0 = X[label == 0, :]
    X1 = X[label == 1, :]
    X2 = X[label == 2, :]

    plt.plot(X0[:, 0], X0[:, 1], 'b^', markersize = 4, alpha = .8)
    plt.plot(X1[:, 0], X1[:, 1], 'go', markersize = 4, alpha = .8)
    plt.plot(X2[:, 0], X2[:, 1], 'rs', markersize = 4, alpha = .8)

    plt.axis('equal')
    plt.plot()
    plt.show()

kmeans_display(X, original_label)

def kmeans_init_centers(X, k):
    # randomly pick k rows of X as initial centers
    return X[np.random.choice(X.shape[0], k, replace=False)]

def kmeans_assign_labels(X, centers):
    # calculate pairwise distances btw data and centers
    D = cdist(X, centers)
    # return index of the closest center
    return np.argmin(D, axis = 1)

def kmeans_update_centers(X, labels, K):
    centers = np.zeros((K, X.shape[1]))
    for k in range(K):
        # collect all points assigned to the k-th cluster
        Xk = X[labels == k, :]
        # take average
        centers[k,:] = np.mean(Xk, axis = 0)
    return centers

def has_converged(centers, new_centers):
    # return True if two sets of centers are the same
    return (set([tuple(a) for a in centers]) ==
        set([tuple(a) for a in new_centers]))

def kmeans(X, K):
    centers = [kmeans_init_centers(X, K)]
    labels = []
    it = 0
    while True:
        labels.append(kmeans_assign_labels(X, centers[-1]))
        new_centers = kmeans_update_centers(X, labels[-1], K)
        if has_converged(centers[-1], new_centers):
            break
        centers.append(new_centers)
        it += 1
    return (centers, labels, it)

(centers, labels, it) = kmeans(X, K)
print('Centers found by our algorithm:')
print(centers[-1])

kmeans_display(X, labels[-1])


# Naive Implementation of SUTD ISTD 2021 50.034 Introduction to Probability and Statistics Midterm Exam Final Question
# Created by James Raphael Tiovalen (2021)

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng()

LAMBDA = rng.uniform(0, 1)
N = 1_000_000
DATA_POINTS = 1_000_000
BINS = 200

# Assume that each call to rand() (i.e., each sample drawn) is independent and identically distributed (i.i.d.)
def rand(num_of_samples):
    return rng.uniform(-LAMBDA, LAMBDA, num_of_samples)

# As N -> +∞, this becomes approximately closer and closer to a normal distribution with mean 0 and variance (N * (LAMBDA ** 2)) / 3
# These properties actually agree with the reformulated/reinterpreted Central Limit Theorem approximation in terms of sums of i.i.d. random variables
def gauss():
    return np.sum(rand(N))

def main():
    nums = np.array([gauss() for _ in np.arange(DATA_POINTS)])
    actual = rng.normal(0, (LAMBDA * np.sqrt(N / 3)), DATA_POINTS)

    # Capture and show all data points within three standard deviations from the mean (≈99.73%)
    bins = np.linspace(
        -3 * (LAMBDA * np.sqrt(N / 3)), 3 * (LAMBDA * np.sqrt(N / 3)), BINS
    )

    # Plot the collected data points to illustrate the normal distribution shape and compare with the actual normal distribution
    plt.style.use("seaborn-deep")
    plt.hist(nums, bins, alpha=0.5, label="nums")
    plt.hist(actual, bins, alpha=0.5, label="actual")
    plt.legend(loc="upper right")
    plt.show()

if __name__ == "__main__":
    main()


"""
Directional filtering functions
"""
import numpy as np
from scipy.fftpack import fft2, ifft2, fftfreq, fftshift, ifftshift

def dff2d(z, dx, thetalow, thetahigh):
    """
    Two-dimensional directional Fourier transform filter.

    Inputs:
    ------
        z: 2D complex array
        dx: grid spacing
        thetalow: low angle for filter (degrees CCW from E)
        thetahigh: high angle for filter (degrees CCW from E)

    Outputs:
    -----
        zf: 2D filtered complex array
    """

    # 2D fourier transfrom
    My, Mx = z.shape
    Z = fft2(z)

    # Compute zonal wavenumbers
    k = fftfreq(Mx, dx/(2*np.pi))
    dk = 1/(Mx*dx)

    # Compute meridional wavenumbers
    l = fftfreq(My, dx/(2*np.pi))
    dl = 1/(My*dx)

    # Need to re-order the FFT output because positive frequencies are output first from the numpy DFT algorithm
    k_r = fftshift(k)
    l_r = fftshift(l)

    # Create a grid for the direction
    Lx,Ly = np.meshgrid(k_r,l_r)
    theta = np.angle(Lx + 1j*Ly)

    thetadeg = np.mod(theta*180/np.pi,360)

    # Create the filter matrix
    H = np.zeros_like(thetadeg)

    if thetahigh > 360:
        filter_idx1 = (thetadeg > thetalow) & (thetadeg < 360)
        filter_idx2 = (thetadeg > 0) & (thetadeg < np.mod(thetahigh,360))
        filter_idx = filter_idx1 | filter_idx2
    else:
        filter_idx = (thetadeg > thetalow) & (thetadeg < thetahigh)

    H[filter_idx] = 1

    # Now reorder H into the original FFT ordering
    H_r = ifftshift(H,axes=1)
    H_r = ifftshift(H_r,axes=0)

    # Finally, filter
    zf = ifft2(Z*H_r)

    return zf

def hilbert_2d(z, dx, dy):
    My, Mx = z.shape
    Z = fft2(z)

    # Compute zonal frequencies
    k = fftfreq(Mx, dx/(2*np.pi))
    dk = 1/(Mx*dx)

    # Compute meridional frequencies
    l = fftfreq(Mx, dx/(2*np.pi))
    dl = 1/(My*dy)

    # Create filter matrices for each of the four quadrant
    Z_posk_posl = np.zeros_like(Z)
    Z_posk_posl[:My//2, :Mx//2] = Z[:My//2, :Mx//2]

    z_posk_posl = ifft2(Z_posk_posl)

    Z_posk_negl = np.zeros_like(Z)
    Z_posk_negl[:My//2, Mx//2::] = Z[:My//2, Mx//2::]

    z_posk_negl = ifft2(Z_posk_negl)

    Z_negk_negl = np.zeros_like(Z)
    Z_negk_negl[My//2::, Mx//2::] = Z[My//2::, Mx//2::]

    z_negk_negl = ifft2(Z_negk_negl)

    Z_negk_posl = np.zeros_like(Z)
    Z_negk_posl[My//2::, :Mx//2] = Z[My//2::, :Mx//2]

    z_negk_posl = ifft2(Z_negk_posl)

    return z_posk_posl, z_posk_negl, z_negk_negl, z_negk_posl