import math

import numpy as N

class GreatCircle:
    """
    from Ed Williams' 'Aviation Formulary'
    (http://williams.best.vwh.net/avform.htm)
    """
    def __init__(self,lon1,lat1,lon2,lat2):
        """
        Define a great circle by specifying:
        lon1 - starting longitude of great circle
        lat1 - starting latitude
        lon2 - ending longitude
        lat2 - ending latitude
        All must be given in degrees.

        Instance variables:
        distance - distance along great circle in radians.
        lon1,lat1,lon2,lat2 - start and end points (in radians).
        antipodal - True if start and end points are antipodal.
        """
        # convert to radians from degrees.
        lat1 = math.radians(lat1)
        lon1 = math.radians(lon1)
        lat2 = math.radians(lat2)
        lon2 = math.radians(lon2)
        self.lat1 = lat1
        self.lat2 = lat2
        self.lon1 = lon1
        self.lon2 = lon2
        # distance along great circle in radians.
        self.distance = 2.*math.asin(math.sqrt((math.sin((lat1-lat2)/2))**2+\
        math.cos(lat1)*math.cos(lat2)*(math.sin((lon1-lon2)/2))**2))
        # check to see if points are antipodal (if so, route is undefined).
        if self.distance == math.pi:
            self.antipodal = True
        else:
            self.antipodal = False

    def points(self,npoints):
        """
        compute arrays of npoints equally spaced
        intermediate points along the great circle.

        input parameter npoints is the number of points
        to compute.

        Returns lons, lats (lists with longitudes and latitudes
        of intermediate points in degrees).

        For example npoints=10 will return arrays lons,lats of 10
        equally spaced points along the great circle.
        """
        # can't do it if endpoints of great circle are antipodal, since
        # route is undefined.
        if self.antipodal:
            raise (ValueError,'cannot compute intermediate points on a great circle whose endpoints are antipodal')
        d = self.distance
        delta = 1.0/(npoints-1)
        f = delta*N.arange(npoints)
        lat1 = self.lat1
        lat2 = self.lat2
        lon1 = self.lon1
        lon2 = self.lon2
        A = N.sin((1-f)*d)/math.sin(d)
        B = N.sin(f*d)/math.sin(d)
        x = A*math.cos(lat1)*math.cos(lon1)+B*math.cos(lat2)*math.cos(lon2)
        y = A*math.cos(lat1)*math.sin(lon1)+B*math.cos(lat2)*math.sin(lon2)
        z = A*math.sin(lat1)               +B*math.sin(lat2)
        lats=N.arctan2(z,N.sqrt(x**2+y**2))
        lons=N.arctan2(y,x)
        lons = map(math.degrees,lons.tolist())
        lats = map(math.degrees,lats.tolist())
        return lons,lats

talks/DevSummit2018/ArcGIS API for Python for ANALYSTS and DATA SCIENTISTS/greatcircle.py


import math

from lebesgue import lebesgue_const
import matplotlib.pyplot as plt
import numpy as np

def equis_nodes(a, b, n):
    return np.linspace(a, b, n)

def cheby_nodes(a, b, n):
    xx = [math.cos((2*x+1)*math.pi/(2*n)) for x in range(0,n)]
    return [ (a+b)/2.0 + (b-a)/2.0 * x for x in xx ]

# Show how the growth of the Lebesgue constant is bound to the number
# of interpolation nodes for the two cases: equispaced and chebyshev.
def grow_test(a, b, nmax):
    xx = np.linspace(a, b, 100)
    nn = range(1, nmax+1)
    cc_equis = [ lebesgue_const(xx, equis_nodes(a,b,n)) for n in nn ]
    cc_cheby = [ lebesgue_const(xx, cheby_nodes(a,b,n)) for n in nn ]
    # Plot stuff follows...
    plt.semilogy(nn, cc_equis, color='blue')
    plt.semilogy(nn, cc_cheby, color='green')
    plt.xlabel('n')
    plt.ylabel('log(lambda)')
    plt.gcf().text(0.2, 0.95, 'equispaced', color='blue')
    plt.gcf().text(0.2, 0.9, 'chebyshev', color='green')
    plt.grid(True)
    plt.show()

# Script entry point
grow_test(-5,5,30)


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

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

# Apply gray scale
gray_img = np.round(0.299 * img[:, :, 0] +
                    0.587 * img[:, :, 1] +
                    0.114 * img[:, :, 2]).astype(np.uint8)

# Prewitt Operator
h, w = gray_img.shape
# define filters
horizontal = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]])  # s2
vertical = np.array([[-1, -1, -1], [0, 0, 0], [1, 1, 1]])  # s1

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

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

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

        # Edge Magnitude
        mag = np.sqrt(pow(horizontalGrad, 2.0) + pow(verticalGrad, 2.0))
        newgradientImage[i - 1, j - 1] = mag

plt.figure()
plt.title('Prewitt_House')
plt.imsave('prewitt_house.jpg', newgradientImage, cmap='gray')
plt.imshow(newgradientImage, cmap='gray')
plt.show()


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

trial = 100000

init = 0.0
states = [init]

for i in range(trial):
    u1 = random.uniform(0.0, 1.0)
    u2 = random.uniform(0.0, 1.0)
    new = math.sqrt(-2.*math.log(u1)) * math.cos(2.0*math.pi*u2)
    states.append(new)

plt.hist(states, 50, density=True)
plt.title('box-muller method for generating standard normal distribution')
plt.savefig('box_muller_standard_normal_distribution.png', dpi=300)
plt.show()

means, variances = [], []
step = trial // 1000
states = np.array(states)
for i in range(1000):
    end = step * (i+1)
    mean = np.mean(states[:end])
    variance = np.var(states[:end])
    means.append(mean)
    variances.append(variance)
means = np.array(means)
variances = np.array(variances)

plt.figure(figsize=(12, 6))
plt.subplot(121)
plt.plot(range(1000), np.abs(0.0-means))
plt.title('convergence rate of mean')
plt.xlabel("iteration (* %d)"%(step))
plt.ylabel("approximation error")

plt.subplot(122)
plt.plot(range(1000), np.abs(1.0-variances))
plt.title("convergence rate of variance")
plt.xlabel("iteration (* %d)"%(step))

plt.ylabel("approximation error")
plt.savefig("box_muller_standard_normal_distribution_convergence_rate.png", dpi=300)
plt.show()


from scipy.stats import beta

def lcb(n: int, k: int, alpha: float):
	"""
	Computes a lower confidence bound on the probability parameter p of a binomial CDF.

	Returns:
		The largest p such that Pr[Binom[n,p] >= k] <= alpha
	"""
	if k == 0:
		return 0
	else:
		# Inspired by https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval#Clopper%E2%80%93Pearson_interval
		return beta.ppf(alpha, k, n-k+1)

def ucb(n: int, k: int, alpha: float):
	"""
	Computes an upper confidence bound on the probability parameter p of a binomial CDF.

	Returns:
		The smallest p such that Pr[Binom[n,p] <= k] <= alpha
	"""
	if k == n:
		return 1
	else:
		return beta.ppf(1-alpha, k+1, n-k)


#!/usr/bin/python
# -*- coding: utf-8 -*-
##########################################################################################
# author: Nikolas Schnellbaecher
# contact: khx0@posteo.net
# date: 2021-02-06
# file: axisPadding.py
# tested with python 3.7.6
##########################################################################################

import numpy as np

def getLinearAxisPadding(xminData: float, xmaxData: float, paddingFraction: float) \
    -> (float, float):
    """
    Input arguments:
    xminData: minimal data value along a given axis
    xmaxData: maximal data value along a given axis
    paddingFraction: Fraction of the data width, that is desired as the padding of this
                     axis. The paddingFraction is a floating point value in [0.0, 1.0].

    Return values:
    xmin, xmax
    """
    dataWidth = xmaxData - xminData
    xmax = xmaxData + dataWidth * paddingFraction
    xmin = xminData - dataWidth * paddingFraction
    return xmin, xmax

def getLogAxisPadding(xminData: float, xmaxData: float, paddingFraction: float) \
    -> (float, float):
    """
    Input arguments:
    xminData: minimal data value along a given axis
    xmaxData: maximal data value along a given axis
    paddingFraction: Fraction of the data width, that is desired as the padding of this
                     axis. The paddingFraction is a floating point value in [0.0, 1.0].

    Return values:
    xmin, xmax

    Same as getLinearAxisPadding taylored for logarithmic axis scaling.

    xminData and xmaxData are the minimal and maximal data values in that axis direction.
    Assuming a logarithmic axis scaling in this direction, this function returns a
    xmin and xmax value pair, such that a desired padding of paddingFraction
    to the left and to the right of the data width along this axis is set.
    This assumes base 10 logarithms and that you want to have an equal padding to the left
    as to the right. Unequal padding at the left and right is not implemented yet.
    The returned xmin, xmax values can then for example be used by the
    ax.set_xlim(xmin, xmax) command, to achieve the desired effect.
    Or alternatively by ax.set_ylim(xmin, xmax) for a given y-axis.

    Example:
    A paddingFraction = 0.04 means
    that the padding between xmaxData and xmax will be 4 percent
    of the data extend (dataWidth). Likewise the padding between xmin and xminData will
    equally be 4 percent of the dataWidth (measured in log-10 decades).

    ### Math description
    Given the minimal and maximal data extend, we define the data width (extent of the data)
    as
    $\Delta w = \log_{10}(xmaxData / xminData) = \log_{10}(xmaxData) - log_{10}(xminData)$
    For the padding along a given axis to the right, we want the following equation to hold
    $\log_{10}(xmax) - \log_{10}(xmaxData) = pF * \Delta w$, where pF is the padding Fraction, and
    xmax the yet unknown maximal value, that we wish to determine. Isolating this last
    equation for xmax simply gives:
    $xmax = xmaxData \cdot 10^{pF * \Delta w}$
    Similarly for the left padding, the governing equation reads
    $\log_{10}(xminData) - \log_{10}(xmin) = pF * \Delta w$,
    assuming an equal padding fraction to the left and to the right. Isolating this equation
    for xmin simply gives
    $xmin = xminData * 10^{-pF * \Delta w}$.
    """

    # data width measured in log-10 decades
    dataWidth = np.log10(xmaxData / xminData)
    xmin = xminData * 10.0 ** (- paddingFraction * dataWidth)
    xmax = xmaxData * 10.0 ** (+ paddingFraction * dataWidth)

    return xmin, xmax

if __name__ == '__main__':

    pass


import sys
try:
    import math
    import sympy as sym
    from sympy import sin,cos,tan,N
except:
    print("Packages not installed. Please install 'sympy' package")
    sys.exit()

print("Box Evolutionary method")
x = sym.Symbol('x')
y = sym.Symbol('y')

def substitute(pt):
    return (f.subs([(x,pt[0]),(y,pt[1])]))

while(True):
    s = ""
    func = input("Enter the function = ")  #x-y+2*(x**2)+2*x*y+y**2
    for i in func:
        s= s+i

    try:
        exec("f = "+s)
        break
    except:
        print("Entered function is incompatible. Try again.")

E = float(input("Enter the epsilon 'E' value = "))  #0.4
x_o = list(map(float,input("Enter the x_o value (give only space between the values) =  ").split()))  #[1,0]
delta = list(map(float,input("Enter the delta value (give only space between the values) =  ").split()))#[1,1]
iteration = 1

def mod_epsilon(arr):
    return math.sqrt(arr[0]**2+arr[1]**2)

while(mod_epsilon(delta) > E):
    print("\niteration = ",iteration)
    iteration = iteration + 1
    print("x_o = {}  delta = {} ==> mod_epsilon = {}".format(x_o,delta,mod_epsilon(delta)))
    x1 = [x_o[0] + delta[1]/2, x_o[1] + delta[1]/2]
    x2 = [x_o[0] + delta[1]/2, x_o[1] - delta[1]/2]
    x3 = [x_o[0] - delta[1]/2, x_o[1] + delta[1]/2]
    x4 = [x_o[0] - delta[1]/2, x_o[1] - delta[1]/2]
    print("x1 = {} x2 = {} x3 = {} x4 = {}".format(x1,x2,x3,x4))
    f_o = substitute(x_o)
    f1 = substitute(x1)
    f2 = substitute(x2)
    f3 = substitute(x3)
    f4 = substitute(x4)
    print("f1 = {} f2 = {} f3 = {} f4 = {}".format(f1,f2,f3,f4,f_o))
    min_f = min(f1,f2,f3,f4,f_o)
    if(min_f == f_o):
        delta[0] = delta[0]/2
        delta[1] = delta[1]/2
        print("minimum = ",x_o)
    elif(min_f == f1):
        x_o = x1
        print("minimum = ",x1)
    elif(min_f == f2):
        x_o = x2
        print("minimum = ",x2)
    elif(min_f == f3):
        x_o = x3
        print("minimum = ",x3)
    elif(min_f == f4):
        x_o = x4
        print("minimum = ",x4)

print(x_o)


"""

"""
import numpy as np
import nashpy as nash

# # prisoners dilemma example 1
# Joe = np.array([[-8, 0], [-10, -1]])
# Bob = np.array([[-8, -10], [0, -1]])
# prisoners_dilemma = nash.Game(Joe, Bob)
# print(prisoners_dilemma)
#
# # find the Nash equilibrium with Support enumeration
# equilibria = prisoners_dilemma.support_enumeration()
# for eq in equilibria:
#     print(eq)

# output: (array([1., 0.]), array([1., 0.])), so (confess, confess) is the Nash equilibrium

# Game 2
A = np.array([[4, 0], [0, 2]])  # A is the row player
B = np.array([[2, 0], [0, 4]])  # B is the column player
game2 = nash.Game(A, B)
print(game2)

# find the Nash equilibrium with support enumeration
equilibria = game2.support_enumeration()
for eq in equilibria:
    print(eq)

# interpretation:
# line 1: (array([1., 0.]), array([1., 0.])). This is the first Nash equilibrium, player A chooses strategy 1, so does
# player B.

# line 2: (array([0., 1.]), array([0., 1.])). This is the second Nash equilibrium, player A chooses strategy 2, so does
# player B.

# Line 3: (array([0.66666667, 0.33333333]), array([0.33333333, 0.66666667])). Means we have mixed strategies, and the
# probabilities assigned to each strategy are that Player A will play strategy 1 66.67% of the time, and strategy 2
# 33.33% of the time. Player B plays strategy 1 66.67% of the time and strategy two 33.33% of the time.

# calculate utilities
sigma_r = np.array([.67, .33])
sigma_c = np.array([.33, .67])
pd = nash.Game(A, B)
print(pd[sigma_r, sigma_c])

# the output is [1.3266 1.3266].


#!/usr/bin/python3
import numpy as np
import matplotlib.pyplot as plt

# Now let's generate this with one of the Markov Chain Monte Carlo methods called Metropolis Hastings algorithm
# Our assumed transition probabilities would follow normal distribution X2 ~ N(X1, Covariance = [[0.2, 0], [0, 0.2]])

import time
start_time = time.time()
# Set up constants and initial variable conditions
num_samples = 100000
prob_density = 0
## Plan is to sample from a Bivariate Gaussian Distribution with mean (0, 0) and covariance of 0.7 between the two variables
mean = np.array([0, 0])
cov = np.array([[1, -0.7], [-0.7, 1]])
cov1 = np.matrix(cov)
mean1 = np.matrix(mean)
x_list, y_list = [], []
accepted_samples_count = 0
## Normalizer of the Probability distribution
## This is not actually required since we are taking ratio of probabilities for inference
normalizer = np.sqrt(((2*np.pi)**2)*np.linalg.det(cov))
## Start with initial Point (0, 0)
x_initial, y_initial = 0, 0
x1, y1 = x_initial, y_initial

for i in range(num_samples):
    ## Set up the Conditional Probability distribution, taking the existing point
    ## as the mean and a small variance = 0.2 so that points near the existing point
    ## have a high chance of getting sampled.
    mean_trans = np.array([x1, y1])
    cov_trans = np.array([[0.2, 0], [0, 0.2]])
    x2, y2 = np.random.multivariate_normal(mean_trans, cov_trans).T
    X = np.array([x2, y2])
    X2 = np.matrix(X)
    X1 = np.matrix(mean_trans)
    ## Compute the probability density of the existing point and the new sampled point
    mahalnobis_dist2 = (X2 - mean1)*np.linalg.inv(cov)*(X2 - mean1).T
    prob_density2 = (1/float(normalizer))*np.exp(-0.5*mahalnobis_dist2)
    mahalnobis_dist1 = (X1 - mean1)*np.linalg.inv(cov)*(X1 - mean1).T
    prob_density1 = (1/float(normalizer))*np.exp(-0.5*mahalnobis_dist1)
    ## This is the heart of the algorithm. Comparing the ration of probability density of the new
    ## point and the existing point(acceptance_ratio) and selecting the new point if it is to have more probability
    ## density. If it has less probability it is randomly selected with the probability of getting
    ## selected being proportional to hte ration of the acceptance ratio
    acceptance_ratio = prob_density2[0, 0] / float(prob_density1[0, 0])
    if (acceptance_ratio >= 1 ) | ((acceptance_ratio < 1) and (acceptance_ratio >= np.random.uniform(0, 1))):
        x_list.append(x2)
        y_list.append(y2)
        x1 = x2
        y1 = y2
        accepted_samples_count += 1
end_time = time.time()
print("Time taken to sample {0} points ==> {1}  seconds".format(accepted_samples_count, end_time - start_time))
print("Acceptance ratio ===> {0}".format(accepted_samples_count/100000))
## Time to display the samples generated
plt.xlabel('X')
plt.ylabel('Y')
plt.scatter(x_list, y_list, color = 'red', alpha = 0.1)
print("Mean of the Sampled Points\n{0} {1}".format(np.mean(x_list), np.mean(y_list)))
print("Covariance matrix of the Sampled Points\n{0}".format(np.cov(x_list, y_list)))
plt.show()


import numpy as np
import matplotlib.pyplot as plt

mph_to_mps = 0.44704

def main():
    altitude_feet = 20
    rope_length_feet = 31
    alpha = np.arcsin(altitude_feet / rope_length_feet)

    speed_x_mph = np.array([3.5, 6, 9, 12, 15])  # miles per hour
    n = speed_x_mph.shape[0]
    speed_x_mps = speed_x_mph*mph_to_mps

    total_force_N = np.array([9, 12, 18, 27.5, 31])
    force_x_N = total_force_N*np.cos(alpha)

    lambd = force_x_N
    H_x = np.stack((speed_x_mps, speed_x_mps**2), axis=1)

    drag_x = np.linalg.lstsq(H_x, lambd, rcond=None)[0]
    print("drag coefficients: ", drag_x)

    F = H_x@drag_x
    plt.plot(speed_x_mps, force_x_N, 'o',
             label='Original data', markersize=10)
    plt.plot(speed_x_mps, F, 'r', label='Fitted line')
    plt.xlabel("Speed in x direction [m/s]")
    plt.ylabel("Force in x direction [N]")
    plt.legend()
    plt.show()

if __name__ == "__main__":
    main()
    print("Done")


"""Module of generic numerical tools """
import numpy as np

def getGeometricSeries(maxval, minval, growthrate, include_zero=True) -> np.ndarray:
    """Return geometric progression based on inputs.

    A geometric progression is defined as the change from n_{i+1} to n_{i} is
    equal to some multiplicative growth rate.

        dn_{i+1} = dn_{i} * r

    or equivalently

        dn_{i+1} = dn_{1} * r^{i-1}

    where r is the growth rate of the series and dn_{i} = n_{i} - n_{i-1}.
    Thus:

        n_i = sum_{j=1}^{i} dn_{1} r^{j-1}

    By assuming n_0 = 0, we use n_{1} = dn_{1} = minval. Whether n_{0} is
    included in the resulting array is determined by the include_zero
    parameter.

    Parameters
    ----------
    maxval : float
        Maximum value that should be reached by series
    minval : float
        Initial value of the series, also sets dn_{1}
    growthrate : float
        Growth rate of the series
    include_zero : bool, optional
        Whether the series should insert a 0.0 at the beginning of the series
        (default is True)

    Returns
    -------
    numpy.ndarray
    """
    # Calculate the number of points required to reach maxval
    npoints = np.log((maxval*(growthrate-1))/minval)/np.log(growthrate)
    npoints = np.floor(npoints).astype(np.int32)

    # Calculate the values of dn_i
    if include_zero:
        geomseries = np.zeros(npoints + 2)
        geomseries[1:-1] = minval*growthrate**np.arange(npoints)
    else:
        geomseries = np.zeros(npoints + 1)
        geomseries[:-1]  = minval*growthrate**np.arange(npoints)

    # Sum the dn_i together to get n_i
    geomseries = np.cumsum(geomseries)

    geomseries[-1] = maxval

    return geomseries

def seriesDiffLimiter(series: np.ndarray, dx=None, magnitude=None) -> np.ndarray:
    """Take a series and limit the difference between successive elements (DBSE)

    This takes in a 1-D array (whose difference between successive elements (DBSE) is
    increasing) and creates a new 1-D array that contains the same initial
    elements, up to a point decided by arguments, and then continues the series
    with constant DBSE.

    By specifying `dx`, the function will keep the first elements in the array
    such that the DBSE is less than `dx`. After that, the new points will be
    filled with intervals of `dx` until they reach the same maximum as the
    original series.

    By specifying `magnitude`, elements in the series that are less than
    magnitude will be duplicated. Elements greater than `magnitude` will be
    replaced such that the DBSE is equal to the spacing of the original series
    at `magnitude`.

    Assumes that the series is increasing and DBSE is increasing. ie. for dxs =
    np.diff(series), dxs[i] < dxs[i+1] for all i.

    Parameters
    ----------
    series : np.ndarray
        Series of floats representing sample points
    dx : float
        The resolution at which to stop growing and maintain resolution.
    magnitude : float
        The magnitude of the series at which to maintain the given resolution.

    Returns
    -------
    np.ndarray

    Examples
    -------

    >> input = [0, 1, 2, 4, 8]
    >> seriesDiffLimiter(input, dx=2.4)
        [0, 1, 2, 4, 6.4, 8]

    Note that the end point is preserved even though the last DBSE is less than
    `dx`.

    >> seriesDiffLimiter(input, magnitude=4.0)
        [0, 1, 2, 4, 6, 8]
    """
    if not (bool(dx) ^ bool(magnitude)): #xnor
        raise RuntimeError('Either dx or magnitude must be set')
    if dx:
        dxs = np.diff(series)
        index = np.ceil( pwlinRoots(np.arange(dxs.size), dxs - dx)[0] ).astype(np.int)
    else:
        index = np.ceil( pwlinRoots(np.arange(series.size), series - magnitude)[0] ).astype(np.int)
        dx = series[index] - series[index-1]

    fill_distance = np.max(series) - series[index]
    fill_size = np.ceil(fill_distance / dx).astype(np.int)
    fill_array = np.arange(1, fill_size+1)*dx + series[index]

    new_series = np.zeros(index + fill_size + 1)
    new_series[:index+1] = series[:index+1]
    new_series[index+1:] = fill_array
    new_series[new_series > series[-1]] = series[-1]

    return new_series

def symmetric2FullTensor(tensor_array) -> np.ndarray:
    """ Turn (n, 6) shape array of tensor entries into (n, 3, 3)

    Assumed that symmtetric entires are in XX YY ZZ XY XZ YZ order."""

    shaped_tensors = np.array([
                     tensor_array[:,0], tensor_array[:,3], tensor_array[:,4],
                     tensor_array[:,3], tensor_array[:,1], tensor_array[:,5],
                     tensor_array[:,4], tensor_array[:,5], tensor_array[:,2]
                    ]).T
    return shaped_tensors.reshape(shaped_tensors.shape[0], 3, 3)

def full2SymmetricTensor(tensor_array) -> np.ndarray:
    """ Turn (n, 3, 3) shape array of tensor entries into (n, 6)

    Symmetric entires are in XX YY ZZ XY XZ YZ order."""
    return np.array([
        tensor_array[:,0,0], tensor_array[:,1,1], tensor_array[:,2,2],
        tensor_array[:,0,1], tensor_array[:,0,2], tensor_array[:,1,2]
                    ]).T

def makeRotationTensor(rotation_axis, theta) -> np.ndarray:
    """Create rotation tensor from axis and angle of rotation

    Uses Rodrigues' rotation formula

    Parameters
    ----------
    rotation_axis : np.ndarray
        Axis to rotate around
    theta : float
        The angle of rotation about the rotation_axis in radians
    """

    eijk = np.zeros((3, 3, 3))
    eijk[0, 1, 2] = eijk[1, 2, 0] = eijk[2, 0, 1] = 1
    eijk[0, 2, 1] = eijk[2, 1, 0] = eijk[1, 0, 2] = -1

    rotation_tensor = np.cos(theta) * np.identity(3) + \
                      (1-np.cos(theta)) * np.einsum('i,j->ij', rotation_axis, rotation_axis) + \
                      -np.sin(theta) * np.einsum('ijk,k->ij', eijk, rotation_axis)

    return rotation_tensor

def rotateTensor(tensor_array, rotation_tensor) -> np.ndarray:
    """Given rotation_tensor, rotate the "value" tensor

    This will infer the type of tensor input (vector, symmetric/full tensor,
    etc.) based on the shape of the array. tensor_array is assumed to be an
    array of the n-rank tensors to be rotated.

    Parameters
    ----------
    tensor_array : np.ndarray
        Array of tensors to be rotated. The type of tensor input is inferred
        from the shape of the array. Shape [n,3] is a vector, [n,6] is a
        symmetric tensor, and [n,9] and [n,3,3] are full rank 2 tensors.
    rotation_tensor : np.ndarray
        The single rank 2 tensor defining rotation.
    """

    def rank2Rotation(rot_tensor, shaped_tensors):
        return np.einsum('ik,ekl,jl->eij', rot_tensor, shaped_tensors, rot_tensor)

    if tensor_array.shape[1] == 3 and tensor_array.ndim == 2:
        return np.einsum('ij,ej->ei', rotation_tensor, tensor_array)

    elif (tensor_array.ndim == 3 and
          all(dimsize == 3 for dimsize in tensor_array.shape[1:]) ):
        return rank2Rotation(rotation_tensor, tensor_array)

    elif tensor_array.shape[1] == 6:
        # Assumed to be symmetric tensor in XX YY ZZ XY XZ YZ order
        shaped_tensors = symmetric2FullTensor(tensor_array)
        rotated_tensor = rank2Rotation(rotation_tensor, shaped_tensors)
        return full2SymmetricTensor(rotated_tensor)

    elif tensor_array.shape[1] == 9:
        shaped_tensors = tensor_array.reshape(tensor_array.shape[0], 3, 3)
        return rank2Rotation(rotation_tensor, shaped_tensors).reshape(tensor_array.shape[0], 9)
    else:
        raise ValueError('Did not find appropriate method'
                           ' for array of shape{}'.format(tensor_array.shape))

def calcStrainRate(velocity_gradient) -> np.ndarray:
    """Calculate strain rate from n velocity gradient tensors

    Interpreted as the symmetric tensor of the velocity gradient:
    1/2 (u_{i,j} + u_{j,i})

    Parameters
    ----------
    velocity_gradient : np.ndarray
        Assumed to be of shape (n,9) in the order XX XY XZ YX YY YZ ZX ZY ZZ

    Returns
    -------
    np.ndarray of shape (n,6) in the order XX YY ZZ XY XZ YZ

    """
    return np.array([
        velocity_gradient[:,0], velocity_gradient[:,4], velocity_gradient[:,8],
        0.5*(velocity_gradient[:,1] + velocity_gradient[:,3]),
        0.5*(velocity_gradient[:,2] + velocity_gradient[:,6]),
        0.5*(velocity_gradient[:,5] + velocity_gradient[:,7]),
                     ]).T

def pwlinRoots(x, y) -> np.ndarray:
    """Find roots of piecewise linear function

    Finds the roots of a piecewise-linear function defined by the nodes x and
    the values at the nodes y.

        f(x[i]) = y[i]    for all i in range(x.size)

    Code taken from: https://stackoverflow.com/a/46911822/7564988

    Parameters
    ----------
    x : (N) ndarray
        1-D array of values corresponding to nodes of the piecewise linear
    y : (N) ndarray
        1-D array of values corresponding to function value at the nodes

    Returns
    -------
    1-D ndarray of locations where y=0
    """

        # Get nodes where the function value changes sign
    s = np.abs(np.diff(np.sign(y))).astype(bool)
        # Perform linear interpolation between the nodes where function
            # changes sign
    return x[:-1][s] + np.diff(x)[s]/(np.abs(y[1:][s]/y[:-1][s])+1)


import numpy as np
import matplotlib.pyplot as plt

def NMF(X, B=None, K=3, divergence="sparse-KL", num_iter=100, eta=1e-3):
    """
    Execute Nonnegative Matrix Factorization (NMF).
    Args :
        X : (M, N). ndarray. input. Each element should be positive.
        B : (M, K). ndarray. (optional). If specified,
                    only Weight matrix will be updated.
        K : int. Decomposition dimension.
        divergence : {"sparse-KL", "EU", "KL", "IS"}. Metrics for divergence.
            "sparse-KL" : Kullback Leibler Divergence with sparse norm.
            "EU" : Squared Euclidean Divergence.
            "KL" : Kullback Leibler Divergence.
            "IS" : Itakura Saito Divergence.
        num_iter : int. Number of iterations.
        eta : float. Updating rate.
    Outputs :
        B : (M, K). ndarray. Basis matrix.
        W : (K, N). ndarray. Weight matrix.
    """
    if X.min() < 0:
        raise ValueError("X should be positive")
    M = X.shape[0]
    N = X.shape[1]

    # Define update function.
    if divergence == "EU":
        def update_matrix(X, B, W, eta=1e-3):
            w_diff = eta * (np.dot(B.T, X) - np.dot(np.dot(B.T, B), W))
            b_diff = eta * (np.dot(X, W.T) - np.dot(np.dot(B,W), W.T))
            return b_diff, w_diff
    elif divergence == "KL":
        def update_matrix(X, B, W, eta=1e-3):
            _X = np.dot(B, W)
            w_diff_denom = B.T.sum(axis=1, keepdims=True)
            b_diff_denom = W.T.sum(axis=0, keepdims=True)
            w_diff =  eta * W/w_diff_denom * (np.dot(B.T, X/_X) -w_diff_denom)
            b_diff = eta * B/b_diff_denom * (np.dot(X/_X, W.T) - b_diff_denom)
            return b_diff, w_diff
    elif divergence == "IS":
        def update_matrix(X, B, W, eta=1e-3):
            _X_ = X/np.dot(B,W)**2
            w_diff_denom = np.dot(B.T, 1.0/np.dot(B,W))
            b_diff_denom = np.dot(1.0/np.dot(B,W), W.T)
            w_diff = eta * W/w_diff_denom * (np.dot(B.T, _X_) - w_diff_denom)
            b_diff = eta * B/b_diff_denom * (np.dot(_X_, W.T) - b_diff_denom)
            return b_diff, w_diff
    elif divergence == "sparse-KL":
        def update_matrix(X, B, W, eta=1e-3):
            raise NotImplementedError
    if type(B) != np.ndarray:
        B = np.random.rand(M, K)
        b_specified = False
    else:
        b_specified = True
    B = np.random.rand(M, K)
    W = np.random.rand(K, N)
    X_max = X.max()
    X /= X_max
    for i in range(num_iter):
        b_diff, w_diff = update_matrix(X, B, W, eta=eta)
        if b_specified:
            pass
        else:
            B += b_diff
        W += w_diff
    W *= X_max
    return B, W

if __name__ == "__main__":
    row = 100
    col = 200
    X = np.sin(np.arange(row)*0.02*np.pi).reshape(row, 1) * np.cos(np.arange(col)*0.04*np.pi).reshape(1, col) + 1.0
    plt.figure(figsize=(5,8))
    plt.title("Original X")
    plt.imshow(X)
    plt.savefig("./fig/org_X.png")
    plt.close()
    for mode in ["EU", "KL", "IS"]:
        if mode == "EU":
            num_iter = 1000
            eta = 1e-3
        elif mode == "KL":
            num_iter = 10000
            eta = 1e-2
        elif mode == "IS":
            num_iter = 10000
            eta = 1e-2
        B, W = NMF(X, K=5, divergence=mode,
                    num_iter=num_iter,
                    eta=eta)
        plt.figure(figsize=(10,8))
        plt.subplot(2,1,1)
        plt.title("Original X")
        plt.imshow(X)
        plt.subplot(2,1,2)
        plt.title(f"BW with {mode}")
        plt.imshow(np.dot(B, W))
        plt.savefig(f"./fig/{mode}_NMF.png")
        plt.close()


import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression, Ridge
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler

# ---------------------------The Normal Equation---------------------
# X = 2 * np.random.rand(100, 1)
# y = 4 + 3 * X + np.random.randn(100, 1)
#
# X_b = np.c_[np.ones((100, 1)), X]  # add x0 = 1 to each instance
# theta_best = np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y)
# X_new = np.array([[0], [2]])
# X_new_b = np.c_[np.ones((2, 1)), X_new]  # add x0 = 1 to each instance
# y_predict = X_new_b.dot(theta_best)
# plt.plot(X_new, y_predict, "r-", linewidth=2, label="Predictions")
# plt.plot(X, y, "b.")
# plt.xlabel("$x_1$", fontsize=18)
# plt.ylabel("$y$", rotation=0, fontsize=18)
# plt.legend(loc="upper left", fontsize=14)
# plt.axis([0, 2, 0, 15])
# plt.show()
# lin_reg = LinearRegression()
# lin_reg.fit(X, y)
# print(lin_reg.intercept_, lin_reg.coef_)
# print(lin_reg.predict(X_new))

# -------------------------Gradient Descent---------------------
# def plot_gradient_descent(theta, eta, theta_path=None):
#     m = len(X_b)
#     plt.plot(X, y, "b.")
#     n_iterations = 1000
#     for iteration in range(n_iterations):
#         if iteration < 10:
#             y_predict = X_new_b.dot(theta)
#             style = "b-" if iteration > 0 else "r--"
#             plt.plot(X_new, y_predict, style)
#         gradients = 2 / m * X_b.T.dot(X_b.dot(theta) - y)
#         theta = theta - eta * gradients
#         if theta_path is not None:
#             theta_path.append(theta)
#     plt.xlabel("$x_1$", fontsize=18)
#     plt.axis([0, 2, 0, 15])
#     plt.title(r"$\eta = {}$".format(eta), fontsize=16)

# theta_path_bgd = []
# np.random.seed(42)
# theta = np.random.randn(2, 1)  # random initialization
# plt.figure(figsize=(10, 4))
# plt.subplot(131);plot_gradient_descent(theta, eta=0.02)
# plt.ylabel("$y$", rotation=0, fontsize=18)
# plt.subplot(132);plot_gradient_descent(theta, eta=0.1, theta_path=theta_path_bgd)
# plt.subplot(133);plot_gradient_descent(theta, eta=0.5)
# plt.show()

# -------------------------Stochastic Gradient Descent-------------------------
# theta_path_sgd = []
# m = len(X_b)
# np.random.seed(42)

# n_epochs = 50
# t0, t1 = 5, 50  # learning schedule hyperparameters

# def learning_schedule(t):
#     return t0 / (t + t1)

# theta = np.random.randn(2,1)  # random initialization
# for epoch in range(n_epochs):
#     for i in range(m):
#         if epoch == 0 and i < 20:                    # not shown in the book
#             y_predict = X_new_b.dot(theta)           # not shown
#             style = "b-" if i > 0 else "r--"         # not shown
#             plt.plot(X_new, y_predict, style)        # not shown
#         random_index = np.random.randint(m)
#         xi = X_b[random_index:random_index+1]
#         yi = y[random_index:random_index+1]
#         gradients = 2 * xi.T.dot(xi.dot(theta) - yi)
#         eta = learning_schedule(epoch * m + i)
#         theta = theta - eta * gradients
#         theta_path_sgd.append(theta)                 # not shown

# plt.plot(X, y, "b.")                                 # not shown
# plt.xlabel("$x_1$", fontsize=18)                     # not shown
# plt.ylabel("$y$", rotation=0, fontsize=18)           # not shown
# plt.axis([0, 2, 0, 15])                              # not shown
# plt.show()
# print(theta)

# -------------------------Polynomial Regression-------------------------

# np.random.seed(42)
# m = 100
# X = 6 * np.random.rand(m, 1) - 3
# y = 0.5 * X**2 + X + 2 + np.random.randn(m, 1)
#
# poly_features = PolynomialFeatures(degree=2, include_bias=False)
# X_poly = poly_features.fit_transform(X)
# lin_reg = LinearRegression()
# lin_reg.fit(X_poly, y)
#
# X_new=np.linspace(-3, 3, 100).reshape(100, 1)
# X_new_poly = poly_features.transform(X_new)
# y_new = lin_reg.predict(X_new_poly)

# for style, width, degree in (("g-", 1, 300), ("b--", 2, 2), ("r-+", 2, 1)):
#     polybig_features = PolynomialFeatures(degree=degree, include_bias=False)
#     std_scaler = StandardScaler()
#     lin_reg = LinearRegression()
#     polynomial_regression = Pipeline([
#         ("poly_features", polybig_features),
#         ("std_scaler", std_scaler),
#         ("lin_reg", lin_reg),
#     ])
#     polynomial_regression.fit(X, y)
#     y_newbig = polynomial_regression.predict(X_new)
#     plt.plot(X_new, y_newbig, style, label=str(degree), linewidth=width)
#
# plt.plot(X, y, "b.", linewidth=3)
# plt.legend(loc="upper left")
# plt.xlabel("$x_1$", fontsize=18)
# plt.ylabel("$y$", rotation=0, fontsize=18)
# plt.axis([-3, 3, 0, 10])
# plt.show()

# -------------------------Learning Curves-------------------------
# def plot_learning_curves(model, X, y):
#     X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2)
#     train_errors, val_errors = [], []
#     for m in range(1, len(X_train)):
#         model.fit(X_train[:m], y_train[:m])
#         y_train_predict = model.predict(X_train[:m])
#         y_val_predict = model.predict(X_val)
#         train_errors.append(mean_squared_error(y_train_predict, y_train[:m]))
#         val_errors.append(mean_squared_error(y_val_predict, y_val))
#         plt.plot(np.sqrt(train_errors), "r-+", linewidth=2, label="train")
#         plt.plot(np.sqrt(val_errors), "b-", linewidth=3, label="val")
#
#
# polynomial_regression = Pipeline([
#     ("poly_features", PolynomialFeatures(degree=10, include_bias=False)),
#     ("lin_reg", LinearRegression()),
# ])
# plot_learning_curves(polynomial_regression, X, y)
# plt.axis([0, 80, 0, 3])                         # not shown in the book
# plt.show()

# -------------------------Regularized Linear Model-------------------------
np.random.seed(42)
m = 20
X = 3 * np.random.rand(m, 1)
y = 1 + 0.5 * X + np.random.randn(m, 1) / 1.5
X_new = np.linspace(0, 3, 100).reshape(100, 1)

def plot_model(model_class, polynomial, alphas, **model_kargs):
    for alpha, style in zip(alphas, ("b-", "g--", "r:")):
        model = model_class(alpha, **model_kargs) if alpha > 0 else LinearRegression()
        if polynomial:
            model = Pipeline([
                ("poly_features", PolynomialFeatures(degree=10, include_bias=False)),
                ("std_scaler", StandardScaler()),
                ("regul_reg", model),
            ])
        model.fit(X, y)
        y_new_regul = model.predict(X_new)
        lw = 2 if alpha > 0 else 1
        plt.plot(X_new, y_new_regul, style, linewidth=lw, label=r"$\alpha = {}$".format(alpha))
    plt.plot(X, y, "b.", linewidth=3)
    plt.legend(loc="upper left", fontsize=15)
    plt.xlabel("$x_1$", fontsize=18)
    plt.axis([0, 3, 0, 4])

plt.figure(figsize=(8,4))
plt.subplot(121)
plot_model(Ridge, polynomial=False, alphas=(0, 10, 100), random_state=42)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.subplot(122)
plot_model(Ridge, polynomial=True, alphas=(0, 10**-5, 1), random_state=42)

plt.show()


import numpy as np

a = np.array([0, 2, 3, 6])
b = np.array([3, 4, 5, 15])

print(np.gcd(a, b))
# [3 2 1 3]

print(type(np.gcd(a, b)))
# <class 'numpy.ndarray'>

l_a = [0, 2, 3, 6]
l_b = [3, 4, 5, 14]

print(np.gcd(l_a, l_b))
# [3 2 1 2]

print(type(np.gcd(l_a, l_b)))
# <class 'numpy.ndarray'>

print(np.gcd(6, 15))
# 3

print(type(np.gcd(6, 15)))
# <class 'numpy.int64'>

a_2d = np.array([[0, 2, 3, 6], [0, 2, 3, 6]])
print(a_2d)
# [[0 2 3 6]
#  [0 2 3 6]]

print(b)
# [ 3  4  5 15]

print(a_2d + b)
# [[ 3  6  8 21]
#  [ 3  6  8 21]]

print(np.gcd(a_2d, b))
# [[3 2 1 3]
#  [3 2 1 3]]

a_mismatch = np.array([0, 1, 2])

# print(np.gcd(a_mismatch, b))
# ValueError: operands could not be broadcast together with shapes (3,) (4,)

print(np.gcd(a, 15))
# [15  1  3  3]

print(np.gcd(15, a))
# [15  1  3  3]


"""
This will be your new module with the Pearson correlation function

First try to fill in the "pearson_1d" function.

When you are done with "pearson_1d", you should be able to run the following
command (from the day5 directory) and see no errors or failures::

    nosetests test_pearson_1d.py

Then, if you are feeling brave, fill in the "pearson_2d" function.  Test with::

    nosetests test_pearson_2d.py
"""
# Python 3 compatibility
from __future__ import print_function, division

import numpy as np

def pearson_1d(x, y):
    """ Pearson product-moment correlation of vectors `x` and `y`

    Parameters
    ----------
    x : array shape (N,)
        One-dimensional array to correlate with `y`
    y : array shape (N,)
        One dimensional array to correlate with `x`

    Returns
    -------
    r_xy : scalar
        Pearson product-moment correlation of vectors `x` and `y`.
    """
    # Mean-center x -> mc_x
    mc_x = x - np.mean(x)
    # Mean-center y -> mc_y
    mc_y = y - np.mean(y)
    # a : Get sum of products of mc_x, mc_y
    a = mc_x.dot(mc_y)
    # b : Get sum of products of mc_x on mc_x
    b = mc_x.dot(mc_x)
    # c : Get sum of products of mc_y on mc_y
    c = mc_y.dot(mc_y)
    # return a / (sqrt(b) * sqrt(c))
    return a / (np.sqrt(b) * np.sqrt(c))

def pearson_2d(x, Y):
    """ Pearson product-moment correlation of vectors `x` and array `Y`

    Parameters
    ----------
    x : array shape (N,)
        One-dimensional array to correlate with every column of `Y`
    Y : array shape (N, P)
        2D array where we correlate each column of `Y` with `x`.

    Returns
    -------
    r_xy : array shape (P,)
        Pearson product-moment correlation of vectors `x` and the columns of
        `Y`, with one correlation value for every column of `Y`.
    """
    # Mean-center x -> mc_x
    mc_x = x - np.mean(x)
    # Mean-center every column of Y -> mc_Y
    mean_Y = np.mean(Y, axis=0)
    mean_Y_expanded = np.tile(mean_Y, (len(x), 1))
    mc_Y = Y - mean_Y_expanded
    # (Hint: np.tile, or (advanced, not yet covered) numpy broadcasting)
    # a : Get sum of products of mc_x and every column of mc_Y
    a = mc_x.dot(mc_Y)
    # b : Get sum of products of mc_x on mc_x
    b = mc_x.dot(mc_x)
    # c : Get sum of products of every column of mc_Y[:, i] on itself
    c = np.sum(mc_Y ** 2, axis=0)
    # return a / (sqrt(b) * sqrt(c))
    return a / (np.sqrt(b) * np.sqrt(c))


"""Provides a k-means algorithm implementation."""
from typing import Callable, List

import numpy as np

def distance_(x: np.ndarray, y: np.ndarray) -> float:
    """Arbitrary distance function."""
    z = x - y
    return np.inner(z, z)

def kmeans(
        input_: np.ndarray,
        k: int,
        steps: int = 0,
        distance: Callable[[np.ndarray, np.ndarray], float] = distance_,
) -> np.ndarray:
    """Randomly calculates k clusters and provides their mean points.

    Args:
        input_: array containing all data points.
        k: number of clusters to be calculated.
        steps: number of iteration steps to be done at most.
        distance: function to calculate distance between two points.

    Returns:
        K points which represent mean point for each cluster.
    """

    if steps < 1:
        steps = 255

    data_count = len(input_)
    cluster_data = np.array([
        input_[i] for i in np.random.choice(data_count, k)
    ])
    found = False
    current_step = 0
    while not found and current_step < steps:
        closest_cluster = np.zeros(data_count)
        near_count = np.zeros(k)
        new_cluster_data = np.zeros(cluster_data.shape)

        for data_index in range(data_count):
            closest_cluster_index = np.argmin([
                distance(input_[data_index], cluster)
                for cluster in cluster_data
            ])
            closest_cluster[data_index] = closest_cluster_index
            near_count[closest_cluster_index] += 1
            new_cluster_data[closest_cluster_index] += input_[data_index]

        from_scratch = False
        cluster_index = 0
        while not from_scratch and cluster_index < k:
            if not near_count[cluster_index]:
                from_scratch = True
                continue
            new_cluster_data[cluster_index] /= near_count[cluster_index]
            cluster_index += 1

        if from_scratch:
            new_cluster_data = [
                input_[i] for i in np.random.choice(data_count, k)
            ]
            current_step += 1
            continue

        if np.array_equal(cluster_data, new_cluster_data):
            found = True
        else:
            cluster_data = new_cluster_data
            current_step += 1

    return cluster_data

def main() -> None:

    try:
        input_: List[np.ndarray] = []
        dimensions = 0

        k = int(input("K: "))
        steps = int(input("Enter maximum number of steps: "))
        n = int(input("Enter number of data points: "))
        for i in range(n):
            input_.append(np.array([
                float(v)
                for v in input(
                    "Enter input values for data point {0}: "
                    .format(i)
                ).split()
            ]))
            if i:
                if dimensions != len(input_[-1]):
                    raise ValueError(
                        "Number of dimensions among data points differ"
                    )
            else:
                dimensions = len(input_[0])

        means = kmeans(np.array(input_), k, steps)

        print("{0}-means:".format(k))
        for i in range(means.shape[0]):
            print("mean {0}: {1}".format(i, means[i]))

    except KeyboardInterrupt:
        pass

if __name__ == "__main__":
    main()


import numpy as np

class KMeans(object):
    def __init__(self, k=2, max_iter=100, init='kmeans++'):
        self.k = k
        self.max_iter = max_iter
        self.init = init

    def _get_label_with_min_distance(self, X, centroids):
        distances = np.linalg.norm(X[:, np.newaxis, :] - centroids, axis=2)
        return distances.argmin(axis=1)

    def _get_init_kmeans_plus(self, X):
        #TODO check this since it should work better
        centroids = np.array([X[np.random.choice(range(X.shape[0]))]])
        for i in range(self.k-1):
            distances = np.linalg.norm(X[:, np.newaxis, :] - centroids, axis=2).sum(1)

            pdf = distances / distances.sum()
            centroid_new = X[np.random.choice(range(X.shape[0]), p=pdf)]
            centroids = np.row_stack((centroids, centroid_new))
        return np.array(centroids)

    def _get_init_random(self, X):
        return np.array([X[np.random.choice(range(n_samples), replace=False)] for k in range(self.k)])

    def _get_init_centroids(self, X):
        if self.init == 'kmeans++':
            return self._get_init_kmeans_plus(X)
        elif self.init == 'random':
            return self._get_init_random(X)

    def fit(self, X):
        # random init of centroids
        n_samples, n_features = X.shape
        centroids = self._get_init_centroids(X)

        clusters = [[] for _ in range(self.k)]
        for i in range(self.max_iter):
            labels = self._get_label_with_min_distance(X, centroids)

            for i, x in enumerate(X):
                clusters[labels[i]].append(x)

            old_centroids = centroids
            centroids = np.array([np.mean(cluster, axis=0) for cluster in clusters])

            if (centroids == old_centroids).all():
                break

        self.centroids = centroids

    def predict(self, X):
        return self._get_label_with_min_distance(X, self.centroids)

if __name__ == "__main__":
    import matplotlib.pyplot as plt
    from sklearn.datasets import make_blobs
    from itertools import product
    random_state = 0
    centers = [[10,7],[0,3],[-5,0],[3,0]]
    n_samples = [500,200,300,100]
    cluster_std=[1.5,1,1,0.5]

    X, y = make_blobs(n_samples=n_samples,
                      random_state=random_state,
                      cluster_std=cluster_std,
                      centers=centers)

    km = KMeans(k=4, max_iter=100)
    km.fit(X)
    y = km.predict(X)

    x1min, x1max = X[:, 0].min(), X[:, 0].max()
    x2min, x2max = X[:, 1].min(), X[:, 1].max()

    X_pred = list(product(np.linspace(x1min-0.5, x1max+0.5, 100),
                          np.linspace(x2min-0.5, x2max+0.5, 100)
                          ))
    X_pred = np.array(X_pred)
    y_pred = km.predict(X_pred)

    plt.scatter(X_pred[:, 0], X_pred[:,1], c=y_pred, alpha=.3, linewidths=0.)
    plt.scatter(X[:, 0], X[:,1], c=y, edgecolors='k')
    plt.show()


import math
import matplotlib.pyplot as plt

nums = [i for i in range(100)]

def avg(array):
    avg = 0
    for element in array:
        avg += element

    avg /= len(array)
    return avg

def std(array):
    std = 0
    mean = avg(array)
    for element in array:
        std += abs(element - mean)**2

    std /= len(array)
    return math.sqrt(std)

even_nums = [i for i in range(0, 100, 2)]

odd_nums = []

for element in nums:
    if element not in even_nums:
        odd_nums.append(element)

avg(even_nums)
std(even_nums)

avg(odd_nums)
std(odd_nums)

excluded_nums = [i for i in range(10,20,1)]
other_excluded = [i for i in range(45,57,1)]

part_c_array = []

for element in nums:
    if element not in excluded_nums and element not in other_excluded:
        part_c_array.append(element)

avg(part_c_array)
std(part_c_array)

part_d_array = []
for element in nums:
    if element in excluded_nums or element in other_excluded:
        part_d_array.append(element)

avg(part_d_array)
std(part_d_array)

# Exercise 1 Part E

x = []

step = 3/100
end = 3
start = 0

for i in range(100):
    x.append(start+(step*i))

print(x)
A = 0.8
B = 2

y = []
for element in x:
    y.append(A*math.exp(element) - B*element)

plt.scatter(x, y)
plt.title("Part E")
#plt.show()

print("Mean of Y: {}".format(avg(y)))

# Part F

def factorial(number):
    if number == 1:
        return 1
    else:
        return number * factorial(number-1)

step = 3/10

x = []

for i in range(10):
    x.append(start+(step*i))

def estimate_ex(x, k):
    final_solution = 0
    final_solution += (x**k)/factorial(k)

y_est = []

for element in x:
    y_est.append(estimate_ex(element, 5))

print(len(x))
print(len(y))
plt.scatter(x, y_est, c='red')
plt.show()

# Seems to compare the same. Built-in should be faster.

math.factorial(5)

# Number 3

import numpy as np

def func_a(x):
    return 2*x*np.sin(0.8*x)

seven_points = np.linspace(1,20, 7)

all_points = [(x, func_a(x)) for x in seven_points]

def determine_closest_points(points, x_point):
    '''
    Determine the closest points for them
    :param points:
    :param x_point:
    :return:
    '''

    point_values = []
    y_values = []
    for index, point in enumerate(points):
        if index+1 > len(points):
            if points[index] < x_point:
                point_values.append(point)
        if points[index][0] < x < points[index+1][0]:
            point_values.append(points[index])
            point_values.append(points[index+1])

    return point_values

def linear_interp(points, new_x_points):
    '''
    Lienarly interpolate for the points
    :param points:
    :param new_x_points:
    :return:
    '''
    new_points = []
    for x_point in new_x_points:
        point_values = determine_closest_points(points, x_point)
        if len(point_values) == 1:
            # Now extrapolating, not interpolating
            return NotImplementedError
        else:
            new_y = ((point_values[1][1] - point_values[0][1])/(point_values[1][0] - point_values[0][0]))*(x_point - point_values[0][0])

        new_points.append((x_point, new_y))

    return new_points

def polynomial_interp(points, new_x_points):
    '''
    Polynomially interpolate points
    :param points:
    :param new_x_points:
    :return:
    '''

    new_points = []
    for x_point in new_x_points:
        point_values = determine_closest_points(points, x_point)
        if len(point_values) == :
            return NotImplementedError
        else:
            new_y = 0

        new_points.append((x_point, new_y))

def neville_interp(points, new_x_points):
    return NotImplementedError


# -*- coding: utf-8 -*-
"""Polynomial Interpolation.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/18YSjC1b3dI7f0ulkSTYbV-3qlbXvstu_

Before you turn this problem in, make sure everything runs as expected. First, **restart the kernel** (in the menubar, select Kernel
→
Restart) and then **run all cells** (in the menubar, select Cell
→
Run All).

Make sure you fill in any place that says YOUR CODE HERE or "YOUR ANSWER HERE", as well as your name and collaborators below:
"""

NAME = ""
COLLABORATORS = ""

"""# **Part 1: Representing a Polynomial**
Polynomials are function of the following format

$$p(x) = a_0 + a_1 x ^ 1 + a_2 x ^ 2 + ... + a_n x ^ n$$

$[a_0, a_1, \cdots a_n]$ are called coefficients and $n$ (called the degree or order) is a non-negative integer

This can also be written as
$$y = f(x) = a_0 x^0 + a_1 x ^ 1 + a_2 x ^ 2 + ... + a_n x ^ n$$

**Example**

For example,
$$y = 1 + 2x^2 + 5x^4$$

is a polynomial of order 4 ($=n$) with $n+1$ coeffecients
$a_0 = 1, a_1 = 0, a_2 = 2, a_3 = 0, a_4 = 5$

# **Method 1: Using List**

---
"""

import numpy as np
import matplotlib.pyplot as plt

# numpy is used for efficient array (vector or matrix) operations
# pyplot is used for plotting
# Must read: [https://www.tutorialspoint.com/numpy/numpy_matplotlib.htm]

a = [1, 0, 2, 0, 5] # coeffecients of the polynomial
n = len(a) - 1 # degree. Remember: number of coeff = degree + 1

'''
For a single value of x, finding p(x)

Note that this is an example of block comment in python. A block comment
starts with three ' and ends with three '.
'''

x = 5.0
p_x = 0.0

for i in range(n + 1):
    '''
    p_x = a[i] * x # WRONG, because no power
    p_x = a[i] * (x ** i) # WRONG, have to add the terms
    '''
    p_x += a[i] * (x ** i) # a ** b means pow(a, b) or a^b

'''
For an array of x, finding p(x) for each element
'''

x_arr = [1.0, 2.0, 3.0, 4.0, 5.0]
p_x_arr = []

'''
# naive way:
for i in range(len(x_arr)):
    print(x_arr[i])
'''

# better way: array traversing
for x in x_arr:
    temp = 0.0
    for i in range(n + 1):
        temp += a[i] * (x ** i)

    p_x_arr.append(temp) # array er last e insert kore dao

print("p({}) =".format(x_arr), p_x_arr)
# note how we formatted the string. A formatted string starts with 'f'.

# Using numpy array for vectorization
import numpy as np
# numpy is used for efficient array (vector or matrix) operations
# Must read: [https://www.tutorialspoint.com/numpy/numpy_matplotlib.htm]

a = np.array([1, 0, 2, 0, 5])
x_arr = np.array([1, 2, 3, 4, 5])
p_x_arr = 0.0

# vectorized version. requires only one loop
for i in range(n + 1):
    p_x_arr += a[i] * (x_arr ** i) # a ** b means pow(a, b) or a^b

print("p({}) =".format(x_arr), p_x_arr)

"""# **Method 2 (Better): Using a Class**

---

Complete the implementation of the polynomial class as showed in the lecture
"""

'''
Lab task 1
Here we implement a Polynomial class with three methods: the constructor
__init__(), the toString method __repr__(), and a method to make the objects
of the class callable, __call__() method
'''

# Polynomial Class

class Polynomial:
    # Constructor, note that it starts and ends with two underscores
    def __init__(self, coeff):
        '''
        Every internal variable of the object must be saved and initialized
        in this method: self.variable = value
        '''
        self.coeff = coeff
        self.degree = len(coeff) - 1

    # Method to make the object callable
    def __call__(self, x_arr):
        '''
        Here we assumed x_arr is a numpy array. Remember that a numpy array acts
        like a vector (1D matrix). So an operation x + 1 would add 1 to each element
        of the matrix (unlike python's defaule list). Simlarly, x ** 2 would return
        element wise square of the array.

        Hence, this method would return an array, where the i'th element is the
        (polynomial) interpolated value of x[i], given the coeffecients a[i].
        '''
        p_x_arr = 0
        # --------------------------------------------
        # HINT: Should look like
        # for i in range(self.degree + 1):
        #     ????
        # --------------------------------------------

        # remember 1: length = degree + 1 for a polynomial
        # remember 2: range(0, a) is same as range(a)
        # remember 3: range(a, b) means a is inclusive, b is exclusive

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

        # YOUR CODE HERE
        for i in range(self.degree + 1):
            p_x_arr += self.coeff[i] * (x_arr ** i)

        return p_x_arr
        # --------------------------------------------

    # String representation method of the object (similar to toString() of java)
    def __repr__(self):
        str_ret = 'Polynomial of degree {}\np(x) = '.format(self.degree)
        for i in range(self.degree + 1):
            a = self.coeff[i]
            if i != 0:
                if a >= 0:
                    str_ret += '+ {}x^{} '.format(a, i)
                else:
                    str_ret += '- {}x^{} '.format(-a, i)
            else:
                str_ret += '{}x^{} '.format(a, i)

        return str_ret

    # custom method 1: to get the degree of the polynomial
    def get_degree(self):
        # --------------------------------------------
        # YOUR CODE HERE
        return self.degree
        # --------------------------------------------

    # custom method 2: to get the coefficients of the polynomial
    def get_coeffs(self):
        # --------------------------------------------
        # YOUR CODE HERE
        return self.coeff
        # --------------------------------------------

# This block is used for testing, it should run without any error.

p = Polynomial(np.array([1.0, 0.0, 2.0, 0.0, 5.0]))
assert p(1) == 8
assert p(6) == 6553
test_x = np.array([1, 6])
test_p_x = np.array([8, 6553])
np.testing.assert_array_equal(p(test_x), test_p_x)

assert p.get_degree() == 4
np.testing.assert_array_equal(p.get_coeffs(),  [1.0, 0.0, 2.0, 0.0, 5.0])
# an example to see if our implementation works
coeff = np.array([1.0, 0.0, 2.0, 0.0, 5.0])
p = Polynomial(coeff)
print(p)  # check if printable
x_arr = np.array([1, 2, 3, 4, 5, 6])
print()
print("p({}) =".format(x_arr), p(x_arr)) # check if the object is callable
# should print p([1 2 3 4 5]) =  [   8.   89.  424. 1313. 3176.]

"""# **Part 2: Polynomial Interpolation (Matrix Method)**
If we have $n+1$ nodes, that is,$\{(x_0, y_0), (x_1, y_1), (x_2, y_2), (x_{n}, y_{n})\}$ that satisfies a polynomial of order
$n$, it can be written as:
\begin{align}
&a_0 + a_1  x_0 + a_2  x_0^2 + \cdots a_n  + x_0^n = y_0\\
&a_0 + a_1  x_1 + a_2  x_1^2 + \cdots a_n  + x_1^n = y_1\\
&a_0 + a_1  x_2 + a_2  x_2^2 + \cdots a_n  + x_2^n = y_2\\
&\cdots\\
&a_0 + a_1  x_{n-1} + a_2  x_{n}^2 + \cdots + a_n  x_{n}^n = y_{n}\\
\end{align}
Here, $p(x) = a_0 + a_1x^1 + a_2x^2 + \cdots a_nx^n$ is called the fitted polynomial of the given data points (nodes). Using this polynomial to find the $y_k$ corresponding to an $x_k$ with the range of the given nodes is called polynomial interpolation.

In matrix form, the equations can be written as $$\mathbf{Xa = y}$$
where $\mathbf{X} =$
\begin{bmatrix}
x_0^0 & x_0^1 & x_0^2 & \cdots & x_0^n\\
x_1^0 & x_1^1 & x_1^2 & \cdots & x_1^n\\
\vdots\\
x_n^0 & x_{n}^1 & x_n^2 & \cdots & x_n^n\\
\end{bmatrix}

$\mathbf{a} =$
\begin{bmatrix}
a_0\\
a_1\\
\vdots\\
a_n
\end{bmatrix}
and $\mathbf{y} =$
\begin{bmatrix}
y_0\\
y_1\\
\vdots\\
y_n
\end{bmatrix}
From this, we can solve for $a$ using
$\mathbf{a = X^{-1}y}$
"""

'''
Lab task 2
Here we implement a function which takes a discrete x and y array, and returns
a Polynomial object (the one we just implemented). This polynomial object can
be used to calculate y for any other value of x (not in that list) within the
range
'''
def get_poly(data_x, data_y):
    n_nodes = len(data_x)
    # np.zeors( (a, b) ) returns a (a x b) matrix, i.e., a rows and b columns
    X = np.zeros( (n_nodes, n_nodes) )

    # See the lecture video how the matrix is formed
    # --------------------------------------------
    # Hint: The code will like like this:
    # for i in range(n_nodes):
    #   for j in range(n_nodes):
    #     X[i, j] = ????
    # --------------------------------------------
    # YOUR CODE HERE
    for i in range(n_nodes):
        for j in range(n_nodes):
            X[i, j] = (data_x[i] ** j)
    # --------------------------------------------
    # We could have also used np.linalg.inv to find the inverse
    # but pinv is more efficient
    X_inv = np.linalg.pinv(X) #pseudo inverse
    a = np.dot(X_inv, data_y)
    p = Polynomial(a)

    return p

# This block is used for testing. It should run without any error

data_x = np.array([-3, -2, -1, 0, 1, 3])
data_y = np.array([-80., -13., 6., 1., 5., 16.])
p = get_poly(data_x, data_y)

assert p.degree == 5
np.testing.assert_array_almost_equal(p.coeff, [ 1., -5.075, 5.52083333, 4.85416667, -1.02083333, -0.27916667])
np.testing.assert_almost_equal(p(-1.5), 1.603515625)
np.testing.assert_almost_equal(p(2), 26.5)

data_x = np.array([-3., -2., -1., 0., 1., 3.])
data_y = np.array([-80., -13., 6., 1., 5., 16.])
p = get_poly(data_x, data_y)
'''
np.linspace(a, b, n) returns a numpy array of n points equally
spaced from a to b
'''
x_arr = np.linspace(-3, 3, 100)
# interpolated values
y_interp = p(x_arr)

# pyplot is used for plotting
# Must read: [https://www.tutorialspoint.com/numpy/numpy_matplotlib.htm]

print(y_interp)

plt.plot(x_arr, y_interp, 'r')
plt.plot(data_x, data_y, 'go')
plt.xlabel('x - axis')
plt.ylabel('y - axis')

plt.show()
# You should get a smooth graph that fits the given data points


import numpy as np
from numpy import linalg as LA
import math

def outer( vects ):
    N = len(vects)

    I = tuple([len(v) for v in vects ])
    T = np.zeros(I, dtype=float)

    for i in np.ndindex(I):
        value = 1
        for n in range(N):
            value *= vects[n][i[n]]

        T[ i ] = value
    return T

def inner(X, Y):
    if X.shape != Y.shape:
        raise ValueError("Invalid matrix dimension, can't multiply "+str(X.shape )+" x "+str(Y.shape))
    return np.sum(np.multiply(X,Y))

def norm(X):
    # Frobenius norm for nway
    return LA.norm(X)

def tucker_operator2(core, facts):
    R = tuple(core.shape)
    N = len(R)
    I = [ facts[n].shape[0] for n in range(N) ]

    if R != [ facts[n].shape[1] for n in range(N) ]:
        raise ValueError("The dimensions between core and factors don't match")

    T = np.zeros(I, dtype=float)
    for r in np.ndindex(R):
        vects = [ facts[n][:,r[n]] for n in range(N) ]
        T += core[r]*outer(vects)

    return T

def tucker_operator(core, facts, order = None):
    R = core.shape
    N = len(R)

    if order is None:
        order = range(N)

    if len(facts) != len(order):
        raise ValueError("Invalid number of factors", len(facts), len(order))

    if [R[n] for n in order] != [ facts[n].shape[1] for n in range(len(order)) ]:
        raise ValueError("The dimensions between core and factors don't match")

    for n in range(len(order)):
        if facts[n].shape[1] != R[order[n]] :
            raise ValueError("Invalid component number in factor "+str(n)+" - ("+str(facts[n].shape[1])+","+str(R[order[n]])+")")

    Tn = core
    Rn = list(R)

    for n in range(len(order)):
        Bn = facts[n]
        Cn = unfold(Tn, order[n])

        Mn = np.dot(Bn, Cn)
        Rn[order[n]] = facts[n].shape[0]

        Tn = refold(Mn, order[n], tuple(Rn))

    return Tn

def kruskal_operator(sigma, facts):
    N = len(facts)
    R = facts[0].shape[1]
    I = [ facts[n].shape[0] for n in range(N) ]

    if sigma is not None and len(sigma)!=R:
        raise ValueError("Size of sigma and components don't match")

    if [ facts[n].shape[1] for n in range(N) ] != [R]*N :
        raise ValueError("The number of components in factors don't match")

    if sigma is None:
        sigma = np.ones(R, dtype=float)

    T = np.zeros(I, dtype=float)
    for r in range(R):
        vects = [ facts[n][:,r] for n in range(N) ]
        T += sigma[r]*outer(vects)

    return T

def hadamard(A, B):
    # Element wise multiplication
    if A.shape != B.shape:
        raise ValueError("The matrices have diferent shapes")

    return np.multiply(A, B)

def kron(A, B):
    # Produto de kroniker
    return np.kron(A, B)

def khatri_rao(A, B):
    if A.shape[1]!= B.shape[1]:
        raise ValueError("Number of colunms between matrices don't match")

    J = A.shape[1]

    T = []
    for j in range(J):
        colj = kron( A[:,j], B[:,j] )
        T.append(colj)

    return np.asarray(T).transpose()

def unfold(T, mod):
    I = T.shape
    N = len(I)

    cmodes = [ v for v in range(N) if v != mod ]
    order = np.concatenate(([mod],cmodes))

    newT = swap(T, order)

    return newT.reshape( [I[mod] , np.asarray([I[m] for m in cmodes ]).prod() ])

def refold(C, mod, I):
    N = len(I)
    newC = C.copy()

    cmodes = [ i for i in range(N) if i != mod ]
    order = list(np.concatenate(([mod],cmodes)))

    In = [ I[o] for o in order ]
    newT = np.reshape( np.asarray(newC), In )
    neworder = [ order.index(i) for i in range(N) ]

    if mod != 0 :
        newT = swap(newT, neworder)

    return newT

def swap(T, order):
    I = T.shape
    N = len(I)

    if( len(order) != N ):
        raise ValueError("Invalid swap order")

    if( type(order) == list ):
        order = np.asarray(order)

    neworder = list(np.arange(N))
    newT = T.copy()

    for i in range(N-1):
        idx = neworder.index(order[i])
        newT = newT.swapaxes(i,idx)
        temp = neworder[i];
        neworder[i] = neworder[idx];
        neworder[idx] = temp;

    return newT


import numpy as np
from scipy import stats
from sklearn.utils import check_array

def partial_correlation(x, y, u):
    """
    Calculates the partial correlation of x and y without the influence
    of u.

    This is useful if the correlation of two variables x and y is conditional to
    the fact that both x and y are correlated with a third variable u.
    For example if x is "consumption of ice at the beach", y is "Number of Shark
    Attacks at the beach" and u is the "Weather condition". Then it should be
    obvious that the correlation between x and y doesn't make any sense, but because
    both x and y are correlated with u, it will have a high correlation. This is
    also known as "spurious correlation".

    Parameters
    ----------
    x : array_like, shape (n,)
        Data.
    y : array_like, shape (n,)
        Data.
    u : array_like, shape (n,)
        The influence of this variable will be removed.

    Returns
    -------
    par_corr : float
        The partial correlation of x and y without the influence of u.
    """
    # r_xy is the estimated correlation between x and y
    r_xy, _ = stats.pearsonr(x, y)
    r_xu, _ = stats.pearsonr(x, u)
    r_yu, _ = stats.pearsonr(y, u)
    par_corr = (r_xy - r_xu * r_yu) / np.sqrt((1 - r_xu) ** 2 * (1 - r_yu) ** 2)
    return par_corr

def multiple_correlation(x, Y, rowvar=False):
    """
    Calculates the multiple correlation of the random variable x to the
    k random Variables in Y.

    The Variables in Y are expected to be in the columns, but you can specify
    it to be row variables with rowvar=True.

    Parameters
    ----------
    x : array_like, shape (n,)
        1-D Data.
    Y : array_like, shape (n, k)
        2-D Data.
    rowvar : bool, default=False
        If True, treat rows of Y as variables and columns as observations.

    Returns
    -------
    mult_corr : float
        The multiple correlation bewteen x and Y.
    """
    Y = check_array(Y)
    if rowvar:
        Y = Y.T
    # pairwise correlation of x and each variable in Y
    r_xY = np.array([corr for corr, _ in [stats.pearsonr(x, y) for y in Y.T]])
    Y_corr = np.corrcoef(Y, rowvar=False)
    mult_corr = np.sqrt(np.linalg.multi_dot([r_xY.T, np.linalg.inv(Y_corr), r_xY]))
    return mult_corr


""" Floyd warshall in numpy and standard implementation """
from numpy import *
inf = float('inf')
graph = [
    [0, 3, 8, inf, -4],
    [inf, 0, inf, 1, 7],
    [inf, 4, 0, inf, inf],
    [2, inf, -5, 0, inf],
    [inf, inf, inf, 6, 0]
]

def make_matrix(file, n):
    graph = [[inf for i in range(n)] for i in range(n)]
    with open(file) as f:
        for l in f:
           (i, j, w) = l.split()
           graph[int(i)-1][int(j)-1] = int(w)
    return graph

def floyd_warshall(graph):
    n = len(graph)
    D = graph
    for k in range(n):
        for i in range(n):
            for j in range(n):
                if i==j:
                    D[i][j] = 0
                else:
                    D[i][j] = min(D[i][j], D[i][k] + D[k][j])
    return D

def fastfloyd(D):
    _, n = D.shape
    for k in range(n):
        i2k = reshape(D[k, :], (1, n))
        k2j = reshape(D[:, k], (n, 1))
        D = minimum(D, i2k + k2j)
    return D.min() if not any(D.diagonal() < 0) else None

def get_min_dist(D):
    if negative_cost_cycle(D):
        return "Negative cost cycle"
    return min(i for d in D for i in d)

def negative_cost_cycle(D):
    n = len(D)
    for i in range(n):
        if D[i][i] < 0:
            return True
    return False

if __name__ == "__main__":
    print(get_min_dist(floyd_warshall(graph)))
    n = 1000
    gr = make_matrix("g1.txt", n)
    D = floyd_warshall(gr)
    print(fastfloyd(array(gr)))
    print(get_min_dist(D))
    print(D)


# dynamic programming | binomial coefficient | Python
# Part of Cosmos by OpenGenus Foundation
import numpy as np

# implementation using dynamic programming
def binomialCoefficient(n, k, C=None):
	if k<0 or n<0:
		raise ValueError('n,k must not be less than 0')

	if k>n:
		return 0

	k = min(k,n-k)

	if C is None:
		C = np.zeros((n+1,k+1))

	if k==0:
		C[n,k] = 1

	if C[n,k] == 0:
		C[n,k] = binomialCoefficient(n-1,k,C=C) + binomialCoefficient(n-1,k-1,C=C)

	return C[n,k]

# test
# print(binomialCoefficient(10,3))
# print(binomialCoefficient(10,6))
# print(binomialCoefficient(10,4))
# print(binomialCoefficient(1,1))
# print(binomialCoefficient(0,0))
# print(binomialCoefficient(0,1))
# print(binomialCoefficient(-1,1))


#!/usr/local/bin/python3
import numpy as np

def ValueIteration( problem ):
    # 1. Initialization
    #       arbitrarily V and PI for all  s ∈ S, except that V (terminal)=0
    V, PI, Gamma = problem.Initialization()  # V[s] should give state value
    theta = 0.0001

    # Loop
    nSweepCount = 0
    while True:  # evaluation , til converge
        delta = 0  # Δ ← 0   Δ control the entire loop, while in policy iteration Δ  only control evaluation loop
        V_prime = V.copy()  # backup, synchronized iteration
        # Loop for each s ∈ S
        for s in problem.AllStates():
            if problem.IsTerminal(s): continue
            # v ← V(s)
            v = V_prime[s]

            # V(s) <- max_a ∑ p( s',r | s, π(s )[ r+ gamma·V(s') ]
            actions = problem.AvailableActions( s )  # here check all available actions
            q_values = np.zeros( len(actions) )
            for i,action in enumerate(actions):
                q_val = 0
                for p, r, s_prime in problem.Successor( s, action ):
                    q_val += p * ( r + Gamma * V_prime[ s_prime ] )  # V_prime here
                q_values[i] = q_val

            # get new value
            # only keep .7 for float number
            V[s] = round( q_values.max(), 7 )

            # Δ ← max( Δ , |v-V(s)| )
            delta = max( delta, abs( v - V[s] ) )

        nSweepCount += 1
        # until  Δ<θ (a small positive number determining the accuracy of estimation)
        print( "debug: evaluation {}th sweep, delta: {}".format( nSweepCount, delta ))
        if delta < theta:
            break

    print( "debug: value converge afte {} sweep".format( nSweepCount ))

    # Output a deterministic policy, π≈π* , such that
    for s in problem.AllStates():
        if problem.IsTerminal(s): continue

        # π(s) = argmax_a ∑ p( s',r | s, π(s )[ r+ gamma·V(s') ]
        actions = problem.AvailableActions( s )  # here check all available actions
        q_values = np.zeros( len(actions) )
        for i,action in enumerate(actions):
            q_val = 0
            for p, r, s_prime in problem.Successor( s, action ):
                q_val += p * ( r + Gamma * V_prime[ s_prime ] )  # V_prime here
            q_values[i] = q_val
        # PI[s] = (q_values.argmax(),)
        PI[s] = tuple([ v for i,v in enumerate(actions) if i in np.where( q_values == q_values.max() )[0] ])

    return V,PI

if __name__ == "__main__":
    from problemCarRental import Problem
    # from problemGridworld import Problem

    problem = Problem()
    ValueIteration( problem )
    problem.Show_cli()


# ------------------------------------------------------------------------------
# Program:     The LDAR Simulator (LDAR-Sim)
# File:        geography.distance
# Purpose:     Calculate distance between two coordinates
#
# Copyright (C) 2018-2021  Intelligent Methane Monitoring and Management System (IM3S) Group
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the MIT License as published
# by the Free Software Foundation, version 3.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# MIT License for more details.

# You should have received a copy of the MIT License
# along with this program.  If not, see <https://opensource.org/licenses/MIT>.
#
# ------------------------------------------------------------------------------

import numpy as np

def get_distance(lon1, lat1,  lon2, lat2,  calc_method):
    '''
    A function that calculate different types of distance between two points

    Parameters
    ----------
    lat1 : latitude 1
    lon1 : longitude 1
    lat2 : latitude 2
    lon2 : longitude 2
    calc_method : Types of distance metrics

    Returns a distance in km

    '''
    lat1 = float(lat1)
    lon1 = float(lon1)
    lat2 = float(lat2)
    lon2 = float(lon2)

    if calc_method == "Euclidean":
        d = ((lon1 - lon2)**2 + (lat1-lat2)**2)**0.5
    elif calc_method == "Haversine":
        radius = 6371.0
        dlat = np.radians(lat2 - lat1)
        dlon = np.radians(lon2 - lon1)
        a = (np.sin(dlat / 2.0) * np.sin(dlat / 2.0) +
             np.cos(np.radians(lat1)) * np.cos(np.radians(lat2)) *
             np.sin(dlon / 2.0) * np.sin(dlon / 2.0))
        c = 2.0 * np.arctan2(np.sqrt(a), np.sqrt(1.0 - a))
        d = radius * c
    elif calc_method == "route":
        # HBD - To be added a later date
        d = 100
    return d


#
# Cox-Ross-Rubinstein Binomial Model
# European Option Valuation
# 05_com/CRR_option_caluation.py
#
# (c) Dr. Yves J. Hilpisch
# Derivatives Analytics with Python
#
import math
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rcParams['font.family'] = 'serif'
from BSM_option_valuation import BSM_call_value

#
# Model Parameters
#
S0 = 100.0  # index level
K = 100.0  # option strike
T = 1.0  # maturity date
r = 0.05  # risk-less short rate
sigma = 0.2  # volatility

# Valuation Function
def CRR_option_value(S0, K, T, r, sigma, otype, M=4):
    ''' Cox-Ross-Rubinstein European option valuation.

    Parameters
    ==========
    S0 : float
        stock/index level at time 0
    K : float
        strike price
    T : float
        date of maturity
    r : float
        constant, risk-less short rate
    sigma : float
        volatility
    otype : string
        either 'call' or 'put'
    M : int
        number of time intervals
    '''
    # Time Parameters
    dt = T / M  # length of time interval
    df = math.exp(-r * dt)  # discount per interval

    # Binomial Parameters
    u = math.exp(sigma * math.sqrt(dt))  # up movement
    d = 1 / u  # down movement
    q = (math.exp(r * dt) - d) / (u - d)  # martingale branch probability

    # Array Initialization for Index Levels
    mu = np.arange(M + 1)
    mu = np.resize(mu, (M + 1, M + 1))
    md = np.transpose(mu)
    mu = u ** (mu - md)
    md = d ** md
    S = S0 * mu * md

    # Inner Values
    if otype == 'call':
        V = np.maximum(S - K, 0)  # inner values for European call option
    else:
        V = np.maximum(K - S, 0)  # inner values for European put option

    z = 0
    for t in range(M - 1, -1, -1):  # backwards iteration
        V[0:M - z, t] = (q * V[0:M - z, t + 1] +
                         (1 - q) * V[1:M - z + 1, t + 1]) * df
        z += 1
    return V[0, 0]

def plot_convergence(mmin, mmax, step_size):
    ''' Plots the CRR option values for increasing number of time
    intervals M against the Black-Scholes-Merton benchmark value.'''
    BSM_benchmark = BSM_call_value(S0, K, 0, T, r, sigma)
    m = range(mmin, mmax, step_size)
    CRR_values = [CRR_option_value(S0, K, T, r, sigma, 'call', M) for M in m]
    plt.figure(figsize=(9, 5))
    plt.plot(m, CRR_values, label='CRR values')
    plt.axhline(BSM_benchmark, color='r', ls='dashed', lw=1.5,
                label='BSM benchmark')
    plt.xlabel('# of binomial steps $M$')
    plt.ylabel('European call option value')
    plt.legend(loc=4)
    plt.xlim(0, mmax)


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sat Nov 25 13:08:50 2017

@author: ratnadeepb
@License: MIT

This modules helps find the norm of an arbitrary vector in a space of arbitrary
dimensions
"""

import numpy as np
import sys

def norm(v):
    try:
        v = np.array(v)
    except:
        sys.exit("A list, tuple or numpy array is expected")

    if v.ndim != 1:
        sys.exit("Not a vector")

    s = 0
    for e in v:
        s += np.square(e)

    return np.sqrt(s)

def scaled_norm(c, v):
    return abs(c) * norm(v)

def unit_vec(v):
    return v / norm(v)

if __name__ == "__main__":
    v = [3, 5]
    print("The norm of v: ", norm(v))
    print()
    print("Norm of v scaled by 2: ", scaled_norm(2, v))
    print()
    print("The unit vector in the direction of v: ", unit_vec(v))


# This program is designed to implement the Simpson's Rule for numerical integration

from __future__ import division
import numpy as np

def Simp(f, a, b, n, args):
	# Inputs:
	# f - the function being integrated
	# a - lower integration limit
	# b - upper integration limit
	# n - the number of "bins" to integrate over
	# args - a tuple of arguments to pass to f

	# Outputs:
	# integral - the approximated value of the integral
	###################################################

	# Calculate the necessary change in x (width of "bin")
	dx = (b - a)/n

	# Calculate initial functional value (at lower limit)
	f0 = f(a,args)

	# Calculate final functional value (at upper limit)
	fn = f(b,args)

	# Create an empty vector to store intermediate functional values to
	f_i = np.zeros(n-1)

	# For each "bin" 1 to n-1, calculate the functional value at the given x, and weight it.
	for i in range(1,n):
		# Even instances receive a weight of 2
		if i%2 == 0:
			x = a + (i*dx)
			f_i[i-1] = 2*f(x,args)
		# Odd instances receive a weight of 4
		else:
			x = a + (i*dx)
			f_i[i-1] = 4*f(x,args)

	# Sum all weighted intermediate functional values
	fi = np.sum(f_i)

	# The integral is equal to the sum of all weighted functional values, times the change in x divided by 3
	integral = (dx/3)*(fi+f0+fn)

	return integral

def simpson(f, lam, lower, upper, n):
	# Inputs:
	# f - function to be integrated
	# lam - a vector of relevant parameter values
	# lower - lower bound of integration
	# upper - upper bound of integration
	# n - the number of "bins" to integrate over

	# Output:
	# solution - vector of results
	###############################

	# create an empty array to store results to
	solution = np.zeros(len(lam))

	# For each value of lambda, the loop will find the value of the integral, and store it to the above vector
	for i in range(len(lam)):
		# Create a tuple that contains a vector of parameters
		parameters = np.array([0.04,lam[i],0.5])
		ptup = (parameters)

		# This takes care of infinite upper bound
		if (upper == np.inf):
			bdiff = 1
			b_0 = 50
			# While the difference between choices of b is still significant, we will continue to increase b
			# When doubling b no longer significantly changes the integral, then the loop breaks.
			while abs(bdiff) > .01:
				b_1 = b_0*2
				result_0 = Simp(f, lower, b_0, n, ptup)
				result_1 = Simp(f, lower, b_1, n, ptup)
				bdiff = result_1 - result_0
				b_0 = b_1
		else:
			b_1 = up

		# Call simp function from above to find the value of the integral.
		simpsol = Simp(f, lower, b_1, n, ptup)

		# Store results
		solution[i] = simpsol

	# The function returns a vector of results. each element of the vector is a different integral value
	return solution


"""
scipy-scipy.signal.lfilter-my_preemphasis_filter.py

This is an example code for scipy.signal.lfilter
https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.lfilter.html

This is an example to create a preemphasis filter
y[n]=x[n]-0.97x[n-1]

"""

from scipy import signal
import numpy as np
import matplotlib.pyplot as plt

t = np.linspace(-1,1,201)
x = np.sin( 2*np.pi*0.75*t + 1.0 )

plt.figure
plt.subplot(211)
plt.plot(t,x,'k-')
plt.grid(True)
# Input signal
plt.title('sin(2*pi*0.75*t+1.0)')

b = [1., -0.97]
a = [1.]
y = signal.lfilter(b, a, x)
# y[n] = x[n] - 0.97 x[n-1]

plt.subplot(212)
plt.plot(t,y,)
plt.grid(True)
# Filtered signal
plt.title('y[n]=x[n]-0.97x[n-1]')


# Figure 20.6, 20.7, 20.8

# PCA train set and test set reconstruction error vs K
# Reconstruction error on test set gets lower as K increased
# Screeplot and fraction of variance explained
# likelihood of PCA model shows “knee” or “elbow” in the curve

import matplotlib.pyplot as plt
import numpy as np
from sklearn.decomposition import PCA
from scipy.stats import multivariate_normal
from tensorflow import keras
import warnings
from sklearn.metrics import mean_squared_error

warnings.simplefilter('ignore', RuntimeWarning) #for some NaN values

# Function to calculate log likelihood of PCA from eigenvalues
# Implemented equations from the book:
# "Probabilistic Machine Learning: An Introduction"

def log_likelihood(evals):

  Lmax = len(evals)
  ll = np.arange(0.0,Lmax)

  for L in range(Lmax):

    group1 = evals[0:L+1] #Divide Eigenvalues in two groups
    group2 = evals[L+1:Lmax]

    mu1 = np.mean(group1)
    mu2 = np.mean(group2)

    # eqn (20.30)
    sigma = (np.sum((group1-mu1)**2 ) + np.sum((group2-mu2)**2)) / Lmax

    ll_group1 = np.sum(multivariate_normal.logpdf(group1, mu1, sigma))
    ll_group2 = np.sum(multivariate_normal.logpdf(group2, mu2, sigma))

    ll[L] = ll_group1 + ll_group2 #eqn (20.31)

  return ll

# Standard mnist dataset

mnist = keras.datasets.mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images[train_labels == 3] # select images of digit '3'

n_images = 1000
train_images = train_images[0:n_images,:,:]
n_samples, n_rows, n_cols = train_images.shape
X = np.reshape(train_images, (n_samples, n_rows*n_cols))

X_train = X[0:int(n_images/2),:] #500 images in train set
X_test = X[int(n_images/2): ,:] #500 images in test set

#Reconstruction error on MNIST vs number of latent dimensions used by PCA

X_rank = np.linalg.matrix_rank(X_train)
K_linspace = np.linspace(1, 0.75*X_rank, 10, dtype=int)
Ks = np.unique(np.append([1, 5, 10, 20], K_linspace))

RMSE_train= np.arange(len(Ks))
RMSE_test = np.arange(len(Ks))

for index,K in enumerate(Ks):
   pca = PCA(n_components = K)

   Xtrain_transformed = pca.fit_transform(X_train)
   Xtrain_proj = pca.inverse_transform(Xtrain_transformed)
   RMSE_train[index] = mean_squared_error(X_train, Xtrain_proj, squared=False)

   Xtest_transformed = pca.transform(X_test)
   Xtest_proj = pca.inverse_transform(Xtest_transformed)
   RMSE_test[index] = mean_squared_error(X_test, Xtest_proj, squared=False)

#profile log likelihood for PCA

n_samples, n_features = X_train.shape
Kmax = min(n_samples, n_features)

pca = PCA(n_components = Kmax)
X_transformed = pca.fit_transform(X_train)
evals = pca.explained_variance_ #eigenvalues in descending order

ll = log_likelihood(evals)

#Fraction of variance explained

fraction_var = np.cumsum(evals[0:50]/np.sum(evals))

#Figure 20.6(a) train set reconstruction error

fig, ax = plt.subplots()
xs = Ks
ys = RMSE_train
plt.title('train set reconstruction error')
plt.xlabel('num PCs')
plt.ylabel('rmse')
ax.plot(xs, ys, marker = 'o')
plt.show()

#Figure 20.6(b) test set reconstruction error

fig, ax = plt.subplots()
xs = Ks
ys = RMSE_test
plt.title('test set reconstruction error')
plt.xlabel('num PCs')
plt.ylabel('rmse')
ax.plot(xs, ys, marker = 'o')
plt.show()

#Figure 20.7(a) Scree plot for training set

fig, ax = plt.subplots()
xs = np.arange(1, 51)
ys = evals[0:50]
plt.title('screeplot')
plt.xlabel('num PCs')
plt.ylabel('eigenvalues')
plt.ticklabel_format(axis="y", style="sci", scilimits=(0,0))
ax.plot(xs, ys)
plt.show()

#Figure 20.7(b) Fraction of variance explained

fig, ax = plt.subplots()
xs = np.arange(1, 51)
ys = fraction_var
plt.xlabel('num PCs')
plt.ylabel('proportion of variance explained')
ax.plot(xs, ys)
plt.show()

#Figure 20.8 Profile likelihood corresponding to PCA model

fig, ax = plt.subplots()
xs = np.arange(1, 51)
ys = ll[0:50]
plt.xlabel('num PCs')
plt.ylabel('profile log likelihood')

ax.plot(xs, ys)
plt.show()


#
#   House_Price_Prediction.py
#
#    This is a very simple prediction of house prices based on house size, implemented
#   in TensorFlow.  This code is part of Pluralsight's course "TensorFlow: Getting Started"
#

import tensorflow as tf
import numpy as np
import math
import matplotlib.pyplot as plt
import matplotlib.animation as animation # import animation support

#  generation some house sizes between 1000 and 3500 (typical sq ft of house)
num_house = 160
np.random.seed(42)
house_size = np.random.randint(low=1000, high=3500, size=num_house )

# Generate house prices from house size with a random noise added.
np.random.seed(42)
house_price = house_size * 100.0 + np.random.randint(low=20000, high=70000, size=num_house)

# Plot generated house and size
plt.plot(house_size, house_price, "bx")  # bx = blue x
plt.ylabel("Price")
plt.xlabel("Size")
plt.show()

# you need to normalize values to prevent under/overflows.
def normalize(array):
    return (array - array.mean()) / array.std()

# define number of training samples, 0.7 = 70%.  We can take the first 70% since the values are randomized
num_train_samples = math.floor(num_house * 0.7)

# define training data
train_house_size = np.asarray(house_size[:num_train_samples])
train_price = np.asanyarray(house_price[:num_train_samples:])

train_house_size_norm = normalize(train_house_size)
train_price_norm = normalize(train_price)

# define test data
test_house_size = np.array(house_size[num_train_samples:])
test_house_price = np.array(house_price[num_train_samples:])

test_house_size_norm = normalize(test_house_size)
test_house_price_norm = normalize(test_house_price)

#  Set up the TensorFlow placeholders that get updated as we descend down the gradient
tf_house_size = tf.placeholder("float", name="house_size")
tf_price = tf.placeholder("float", name="price")

# Define the variables holding the size_factor and price we set during training.
# We initialize them to some random values based on the normal distribution.
tf_size_factor = tf.Variable(np.random.randn(), name="size_factor")
tf_price_offset = tf.Variable(np.random.randn(), name="price_offset")

# 2. Define the operations for the predicting values - predicted price = (size_factor * house_size ) + price_offset
#  Notice, the use of the tensorflow add and multiply functions.  These add the operations to the computation graph,
#  AND the tensorflow methods understand how to deal with Tensors.  Therefore do not try to use numpy or other library
#  methods.
tf_price_pred = tf.add(tf.multiply(tf_size_factor, tf_house_size), tf_price_offset)

# 3. Define the Loss Function (how much error) - Mean squared error
tf_cost = tf.reduce_sum(tf.pow(tf_price_pred-tf_price, 2))/(2*num_train_samples)

# Optimizer learning rate.  The size of the steps down the gradient
learning_rate = 0.1

# 4. define a Gradient descent optimizer that will minimize the loss defined in the operation "cost".
optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(tf_cost)

1. Simple Linear Price Prediction Model/House Price Prediction.py


# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: percent
#       format_version: '1.3'
#       jupytext_version: 1.4.2
#   kernelspec:
#     display_name: Python [conda env:mdd] *
#     language: python
#     name: conda-env-mdd-py
# ---

# %% [markdown]
# # Fourier transforms
#
# ## From very naive approximations of the spectrum to more complex examples:
#

# %% jupyter={"outputs_hidden": false}
from scipy import fft
import numpy as np
import matplotlib.pyplot as plt

# %matplotlib inline

# %% [markdown]
# ## Two basic ploting functions that repeat the actuall spectral analysis:

# %% jupyter={"outputs_hidden": false}
import sys
sys.path.append('../../scripts')
from signal_processing import *

# %% [markdown]
# ## More elaborate example, demonstration of a leakage effect:

# %% jupyter={"outputs_hidden": false}
# We sample a signal at fs = 200 Hz and record 256 points"

# true values
A = 1.0 # Volt, amplitude
ff = 10.0 # Hz, signal frequency, zero harmonics

# We will work with different sampling frequencies
# and different lengths of records:

fs = 200.0 #Hz
N = 256 # points

plotSignal(A,ff,fs,N)

# %% [markdown]
# ### Note:
# 1. leakage at around 10 Hz
# 2. peak is below 1 Volt
# 3. we can get the spectrum up to half of sampling frequency
# 4. frequency resolution is $\Delta f = 1/T = 0.781$ Hz
#
# ### Let us try to minimize the leakage:
# #### Let's first increase the sampling rate, stay with N = 256

# %% jupyter={"outputs_hidden": false}
fs = 1000.0 #Hz
N = 256 # points
plotSignal(A,ff,fs,N)

# %% [markdown]
# ### Note:
# 1. resolution in time is great BUT:
# 1. resolution in frequency is worse, $\Delta f = 1/T = 1/0.25 = 3.91$ Hz
# 2. we see up to 500 Hz, but have here only 128 useful points
# 3. leakage is severe, the peak amplitude is about 0.66 Volt
# 4. peak location is now 11.7 Hz
#
#
# <span style="color:red"> **MORE IS NOT NECESSARILY BETTER** </span>
#
# ###Let us try to minimize the leakage problem sampling closer to the Nyquist frequency

# %% jupyter={"outputs_hidden": false}
fs = 25.0 #Hz
N = 256 # points
plotSignal(A,ff,fs,N)

# %% [markdown]
# ### Note:
# 1. sine does not seem to be a sine wave, time resolution is bad for Nyquist frequency
# 2. frequency resolution is better, about 0.1 Hz
# 3. peak is about 0.75 Volt, less leakage but still strong
# 4. peak location is close, 9.96 Hz
#
# ### Let's get more points, keep same sampling frequency

# %% jupyter={"outputs_hidden": false}
fs = 25.0 #Hz
N = 512 # points
plotSignal(A,ff,fs,N)

# %% [markdown]
# ### Note:
# 1. time sampling is much longer, $T = N/f_s = 512/(25 Hz) = 20.48$ s
# 2. peak is narrow, at 10.01 Hz
# 3. resolution is good, much lower leakage, value is close to 0.9 Volt
#
# ### Still, how to get the *perfect* spectrum? Use tricks, for instance, sample at a specific frequency:

# %% jupyter={"outputs_hidden": false}
fs = 25.6 #Hz
N = 256 # points
plotSignal(A,ff,fs,N)

# %% jupyter={"outputs_hidden": false}
# After you know the real value, you can save a lot of time:
fs = 25.6 #Hz
N = 64 # points
plotSignal(A,ff,fs,N)


# Compute 95% CI for a beta distribution
import superimport

from scipy.stats import beta
import numpy as np
np.random.seed(42)

N1 = 2; N0 = 8; N = N0 + N1 # Sufficient statistics
aprior = 1; bprior = 1; # prior
apost = aprior + N1; bpost = bprior + N0 # posterior

alpha = 0.05
CI1 = beta.interval(1-alpha, apost, bpost)
print('{:0.2f}--{:0.2f}'.format(CI1[0], CI1[1])) # (0.06:0.52)

l  = beta.ppf(alpha/2, apost, bpost)
u  = beta.ppf(1-alpha/2, apost, bpost)
CI2 = (l,u)
print('{:0.2f}--{:0.2f}'.format(CI2[0], CI2[1])) # (0.06:0.52)

samples = beta.rvs(apost, bpost, size=1000)
samples = np.sort(samples)
CI3 = np.percentile(samples, 100*np.array([alpha/2, 1-alpha/2]))
print('{:0.2f}--{:0.2f}'.format(CI3[0], CI3[1])) # (0.06:0.51)


import numpy as np

EPSILON = 1e-10

def _error(actual: np.ndarray, predicted: np.ndarray):
    """ Simple error """
    return actual - predicted

def mse(actual: np.ndarray, predicted: np.ndarray):
    """ Mean Squared Error """
    return np.mean(np.square(_error(actual, predicted)))

def rmse(actual: np.ndarray, predicted: np.ndarray):
    """ Root Mean Squared Error """
    return np.sqrt(mse(actual, predicted))

def smape(actual: np.ndarray, predicted: np.ndarray):
    """
    Symmetric Mean Absolute Percentage Error
    Note: result is NOT multiplied by 100
    """
    return np.mean(2.0 * np.abs(actual - predicted) / ((np.abs(actual) + np.abs(predicted)) + EPSILON))


"""
newton_algorithm.py

Returns the minimizer of the function
X0 - initial guess
func - anonimous function
func_grad - anonimous function gradient
func_hessian - anonimous function hessian
"""

import numpy as np

def alpha_linesearch_secant_algorithm(X, func_grad, d):

    epsilon = 10e-3;
    N_iter_max = 100;
    alpha_curr = 0;
    alpha = 0.5;
    obj_func_dir_derivative_at_X0 = func_grad(X).T @ d; # directional derivative
    obj_func_dir_derivative = obj_func_dir_derivative_at_X0; # directional derivative

    for iter_no in range(1, N_iter_max + 1):
        alpha_old = alpha_curr;
        alpha_curr = alpha;
        obj_func_dir_derivative_old = obj_func_dir_derivative; # directional derivative
        obj_func_dir_derivative = func_grad(X + alpha_curr * d).T @ d; # directional derivative

        if (obj_func_dir_derivative < epsilon):
            break;

        alpha = (obj_func_dir_derivative * alpha_old - obj_func_dir_derivative_old * alpha_curr) / (obj_func_dir_derivative - obj_func_dir_derivative_old);

        if (np.abs(obj_func_dir_derivative) < epsilon * np.abs(obj_func_dir_derivative_at_X0)):
            break;

        if (iter_no == N_iter_max):
            print('Terminating line search with number of iterations: %d' % (iter_no));

    return alpha;

def newton_algorithm(X0, func, func_grad, func_hessian, options):

    epsilon = 10e-6
    reg_coeff = 1;
    report = {};
    N_iter_max = options['N_iter_max'];
    tolerance_x = options['tolerance_x'];
    tolerance_y = options['tolerance_y'];
    progress_x = np.zeros((X0.size, N_iter_max + 1));
    progress_y = np.zeros((1, N_iter_max + 1));
    progress_x[:, [0]] = X0;
    progress_y[0, [0]] = func(X0);
    X_old = X0;

    for iter_no in range(1, N_iter_max + 1):
        grad = func_grad(X_old);

        if (np.linalg.norm(grad) < epsilon):
            print('norm(grad) < epsilon in %d iterations, exit..' % (iter_no));
            break;

        d = np.linalg.solve(func_hessian(X_old) + reg_coeff * np.eye(X_old.size, X_old.size), -grad); # directional vector, Levenberg-Marquardt modification
        X = X_old + d;
        progress_x[:, [iter_no]] = X;
        progress_y[0, [iter_no]] = func(X);

        if (np.linalg.norm(X - X_old) < tolerance_x * np.linalg.norm(X_old)):
            print('Tolerance in X is reached in %d iterations, exit..' % (iter_no));
            break;

        if (np.abs(progress_y[0, [iter_no]] - progress_y[0, [iter_no - 1]]) < tolerance_y * np.abs(progress_y[0, [iter_no - 1]])):
            print('Tolerance in Y is reached in %d iterations, exit..' % (iter_no));
            break;

        X_old = X;

    report = {'N_iter_max' : N_iter_max, 'iter_no' : iter_no, 'X0' : X0, 'X' : X, 'progress_x' : progress_x, 'progress_y' : progress_y};
    return (X, report);

def newton_algorithm_linesearch(X0, func, func_grad, func_hessian, options):

    epsilon = 10e-6
    reg_coeff = 1;
    report = {};
    N_iter_max = options['N_iter_max'];
    tolerance_x = options['tolerance_x'];
    tolerance_y = options['tolerance_y'];
    progress_x = np.zeros((X0.size, N_iter_max + 1));
    progress_y = np.zeros((1, N_iter_max + 1));
    progress_x[:, [0]] = X0;
    progress_y[0, [0]] = func(X0);
    X_old = X0;

    for iter_no in range(1, N_iter_max + 1):
        grad = func_grad(X_old);

        if (np.linalg.norm(grad) < epsilon):
            print('norm(grad) < epsilon in %d iterations, exit..' % (iter_no));
            break;

        d = np.linalg.solve(func_hessian(X_old) + reg_coeff * np.eye(X_old.size, X_old.size), -grad); # directional vector, Levenberg-Marquardt modification
        alpha = alpha_linesearch_secant_algorithm(X_old, func_grad, d); # step size
        X = X_old + alpha * d;
        progress_x[:, [iter_no]] = X;
        progress_y[0, [iter_no]] = func(X);

        if (np.linalg.norm(X - X_old) < tolerance_x * np.linalg.norm(X_old)):
            print('Tolerance in X is reached in %d iterations, exit..' % (iter_no));
            break;

        if (np.abs(progress_y[0, [iter_no]] - progress_y[0, [iter_no - 1]]) < tolerance_y * np.abs(progress_y[0, [iter_no - 1]])):
            print('Tolerance in Y is reached in %d iterations, exit..' % (iter_no));
            break;

        X_old = X;

    report = {'N_iter_max' : N_iter_max, 'iter_no' : iter_no, 'X0' : X0, 'X' : X, 'progress_x' : progress_x, 'progress_y' : progress_y};
    return (X, report);