# -*- coding: utf-8 -*-

import numpy as np
import numpy.linalg as la

class Kernel(object):
    """
    Implemented following definitions at found at http://en.wikipedia.org/wiki/Support_vector_machine

    For these ones do I have to choose my kernel and pass arguments defined in the outer def,
    later passing x and y args to the  nested f function? How does this work?
    """
    @staticmethod
    def linear_kernel(**kwargs):
        def f(x1, x2):
            return np.inner(x1, x2) # inner is the dot product
        return f

    @staticmethod
    def gaussian(sigma):
        def f(x, y):
            left = la.norm(x-y) ** 2
            right = (2 * sigma ** 2)
            exponent = np.sqrt(left / right) * -1
            return np.exp(exponent)
        return f

    @staticmethod
    def polynomial_kernel(exponent, coef, **kwargs):
        def f(x1, x2):
            dotProduct = np.inner(x1, x2)
            return (dotProduct + coef)**exponent
        return f


import numpy as np

def areCospectral(A, B):
    """Input: 2 quadratic matrices of size n. Output: True/False depending if A and B are cospectral."""

    A_evals, _ = np.linalg.eig(A) # A_evecs
    B_evals, _ = np.linalg.eig(B) # B_evecs

    cospectral = validateEigenvalues(A_evals, B_evals)

    if cospectral:
        return True
    else:
        return False

def validateEigenvalues(A_evals, B_evals, err = 2 ** (-20)):
    """Input: 2 vectors containing the eigenvalues of 2 matrices.
    Output: True if the difference in all values is smaller then a given error."""

    evals = []

    for i in range(len(A_evals)):
        if A_evals[i] - B_evals[i] > err:
            return False
        evals.append(round(A_evals[i], 2))
    print(evals)

    return True

A = [
    [0,0,1,0,1],
    [0,0,0,0,0],
    [1,0,0,1,0],
    [0,0,1,0,1],
    [1,0,0,1,0]
]

B = [
    [0,1,0,0,0],
    [1,0,1,1,1],
    [0,1,0,0,0],
    [0,1,0,0,0],
    [0,1,0,0,0]
]

print(areCospectral(A, B))


import numpy as np

def AND(x1, x2):
    x = np.array([x1, x2])
    w = np.array([0.5, 0.5])
    b = -0.7
    return int((np.sum(w*x) + b) > 0)

def NAND(x1, x2):
    x = np.array([x1, x2])
    w = np.array([-0.5, -0.5])
    b = 0.7
    return int((np.sum(w*x) + b) > 0)

def OR(x1, x2):
    x = np.array([x1, x2])
    w = np.array([0.5, 0.5])
    b = -0.2
    return int((np.sum(w*x) + b) > 0)

def XOR(x1, x2):
    return AND(NAND(x1, x2), OR(x1, x2))

print("AND")
print(AND(0, 0))
print(AND(1, 0))
print(AND(0, 1))
print(AND(1, 1))

print("NAND")
print(NAND(0, 0))
print(NAND(1, 0))
print(NAND(0, 1))
print(NAND(1, 1))

print("OR")
print(OR(0, 0))
print(OR(1, 0))
print(OR(0, 1))
print(OR(1, 1))

print("XOR")
print(XOR(0, 0))
print(XOR(1, 0))
print(XOR(0, 1))
print(XOR(1, 1))


import numpy as np

class MeanSquaredLogarithmicError:
    def __call__(self, y: np.ndarray, y_pred: np.ndarray) -> np.float64:
        return self.loss(y, y_pred)

    def loss(self, y: np.ndarray, y_pred: np.ndarray) -> np.float64:
        return np.sum(np.power(np.log(1 + y) - np.log(1 + y_pred), 2)) / y.shape[0]


import numpy as np
import matplotlib.pyplot as plt
import cvxpy as cvx

f = np.array( [1,1] )
A = np.array( [[2,1], [1,2]] )
b = np.array( [[29],[25]] )
lb = np.array( [[2],[5]] )

x = cvx.Variable(2,1)

objective = cvx.Minimize( -f*x )
constraints = [ A*x <=b, lb <=x ]

prob = cvx.Problem( objective, contraints )
result = prob.solve()

print(x.value)
print(result)

   # A = np.array( [2,1], [1,2] )
# => A = np.array( [[2,1], [1,2]] )
# Traceback (most recent call last):
  # File "cvxpy-example-linear_programming-01.py", line 6, in <module>
    # A = np.array( [2,1], [1,2] )
# TypeError: data type not understood


import numpy as np
from scipy.special import i0

def vonmises_kde(data, kappa, n_bins):
    """
    Generates a Vonmises kernel density estimate (KDE) for a given array
    of angles in radians.
    :param data: Angles in radians
    :param kappa: Vonmises kappa parameter
    :param n_bins: How many bins to generate the distribution over
    :return: Bins, Vonmises kDE
    """
    # from https: // stackoverflow.com / questions / 28839246 /
    # scipy - gaussian - kde - and -circular - data
    bins = np.linspace(-np.pi, np.pi, n_bins)
    x = np.linspace(-np.pi, np.pi, n_bins)
    # integrate vonmises kernels
    kde = np.exp(kappa * np.cos(x[:, None] - data[None, :])).sum(1) / (
        2 * np.pi * i0(kappa)
    )
    kde /= np.trapz(kde, x=bins)
    return bins, kde


from sympy.solvers import solve
from sympy import Symbol
from sympy import *
import matplotlib.pyplot as plt
import numpy as np
from scipy.sparse import *
from numpy.linalg import inv
from array import *
from scipy import linalg
x=Symbol('x')

# function to find k and f for linear elements
def kfmatixLin(nEle,domainLen,a,c,f):
    xb=[]
    xa=[]
    for i in range(1,nEle+1):
        xb.append(i*(domainLen/nEle))
        xa.append((i-1)*(domainLen/nEle))
    k=[]
    Ftemp=[]
    # for ith element  # NOTE: [[element 1 k's],[element 2 k's], ...]
    for i in range(nEle):
        psi_1 = (xb[i]-x)/(xb[i]-xa[i])
        psi_2 = (x-xa[i])/(xb[i]-xa[i])
        # print(psi_1)
        # print(psi_2)
        k.append([])
        Ftemp.append(integrate(f*psi_1 ,(x, xa[i], xb[i])))
        Ftemp.append(integrate(f*psi_2 ,(x, xa[i], xb[i])))
        k[i].append(integrate( a*diff(psi_1,x)*diff(psi_1,x)+c*psi_1*psi_1,(x, xa[i], xb[i])))
        k[i].append(integrate( a*diff(psi_1,x)*diff(psi_2,x)+c*psi_1*psi_2,(x, xa[i], xb[i])))
        k[i].append(integrate( a*diff(psi_2,x)*diff(psi_1,x)+c*psi_2*psi_1,(x, xa[i], xb[i])))
        k[i].append(integrate( a*diff(psi_2,x)*diff(psi_2,x)+c*psi_2*psi_2,(x, xa[i], xb[i])))

    F=[]
    F.append(Ftemp[0])
    for i in range(0,len(Ftemp)-2,2):
        F.append(Ftemp[i+1]+Ftemp[i+2])
    F.append(Ftemp[len(Ftemp)-1])

    # print('k = ',k)
    # print('Ftemp',Ftemp)
    # print('F = ',F)
    # two diagonals of the tridiagonal matrix
    diagA=[]
    diagB=[]
    # for three element 4*4 k matrix
    for i in range(nEle):
        diagA.append(k[i][1])
    # print('diagA',diagA)
    # NOTE: no need for diagC as it will always be same as diagA

    diagB.append(k[0][0])
    for i in range(nEle-1):
        diagB.append(k[i][3]+k[i+1][0])
    diagB.append(k[nEle-1][3])
    # print('diagB',diagB)
    diagA=np.array(diagA, dtype=np.float64)
    diagB=np.array(diagB, dtype=np.float64)

    K = np.array( diags([diagB,diagA,diagA], [0,-1, 1]).todense() )
    return(K,F)
# function to find k and f for Quadratic elements
def kfmatixQad(nEle,domainLen,a,c,f):
    xb=[]
    xa=[]
    xc=[]
    for i in range(1,nEle+1):
        xb.append(i*(domainLen/nEle))
        xa.append((i-1)*(domainLen/nEle))
    for i in range(nEle):
        xc.append(0.5*xa[i]+0.5*xb[i])
    # print('xa',xa)
    # print('xb',xb)
    # print('xc',xc)
    k=[]
    Ftemp=[]
    # for ith element  # NOTE: [[element 1 k's],[element 2 k's], ...]
    for i in range(nEle):
        psi_1 = ((x-xc[i])*(x-xb[i]))/((xa[i]-xc[i])*(xa[i]-xb[i]))
        psi_2 = ((x-xa[i])*(x-xb[i]))/((xc[i]-xa[i])*(xc[i]-xb[i]))
        psi_3 = ((x-xa[i])*(x-xc[i]))/((xb[i]-xa[i])*(xb[i]-xc[i]))
        # print(psi_1)
        # print(psi_2)
        k.append([])
        Ftemp.append(integrate(f*psi_1 ,(x, xa[i], xb[i])))
        Ftemp.append(integrate(f*psi_2 ,(x, xa[i], xb[i])))
        Ftemp.append(integrate(f*psi_3 ,(x, xa[i], xb[i])))
        k[i].append(integrate( a*diff(psi_1,x)*diff(psi_1,x)+c*psi_1*psi_1,(x, xa[i], xb[i])))
        k[i].append(integrate( a*diff(psi_1,x)*diff(psi_2,x)+c*psi_1*psi_2,(x, xa[i], xb[i])))
        k[i].append(integrate( a*diff(psi_1,x)*diff(psi_3,x)+c*psi_1*psi_3,(x, xa[i], xb[i])))

        k[i].append(integrate( a*diff(psi_2,x)*diff(psi_1,x)+c*psi_2*psi_1,(x, xa[i], xb[i])))
        k[i].append(integrate( a*diff(psi_2,x)*diff(psi_2,x)+c*psi_2*psi_2,(x, xa[i], xb[i])))
        k[i].append(integrate( a*diff(psi_2,x)*diff(psi_3,x)+c*psi_2*psi_3,(x, xa[i], xb[i])))

        k[i].append(integrate( a*diff(psi_3,x)*diff(psi_1,x)+c*psi_3*psi_1,(x, xa[i], xb[i])))
        k[i].append(integrate( a*diff(psi_3,x)*diff(psi_2,x)+c*psi_3*psi_2,(x, xa[i], xb[i])))
        k[i].append(integrate( a*diff(psi_3,x)*diff(psi_3,x)+c*psi_3*psi_3,(x, xa[i], xb[i])))

    F=[]
    F.append(Ftemp[0])
    F.append(Ftemp[1])
    t=0
    i=0
    while t<nEle-1:
        F.append(Ftemp[i+2]+Ftemp[i+3])
        F.append(Ftemp[i+4])
        i=i+3
        t=t+1
    F.append(Ftemp[len(Ftemp)-1])

    # print('k = ',k)
    # print('Ftemp',Ftemp)
    # print('F = ',F)
    # print('len f',len(F))
    # three diagonals of the quadiagonal matrix
    diagA=[]
    diagB=[]
    diagC=[]
    # for three element 4*4 k matrix
    for i in range(nEle):
        diagA.append(k[i][1])
        diagA.append(k[i][5])
    # print('diagA',diagA)

    for i in range(nEle):
        diagC.append(k[i][2])
        if i!=nEle-1:
            diagC.append(0.0)
    # print('diagC',diagC)

    diagB.append(k[0][0])
    diagB.append(k[0][4])
    for i in range(nEle-1):
        diagB.append(k[i][8]+k[i+1][0])
        diagB.append(k[i+1][4])
    diagB.append(k[nEle-1][8])

    # print('diagB',diagB)
    diagA=np.array(diagA, dtype=np.float64)
    diagB=np.array(diagB, dtype=np.float64)
    diagC=np.array(diagC, dtype=np.float64)

    K = np.array(diags([diagB,diagA,diagA,diagC,diagC], [0,-1, 1,-2, 2]).todense() )
    return(K,F)
# function to find Q at the left and right ends
def solQ(k,f,nEle,bcs,method):
    bc1=Symbol('bc1')
    bc2=Symbol('bc2')
    q1=Symbol('q1')
    q2=Symbol('q2')
    c1=0
    c2=0
    if method=='linear':
        for i in range(nEle+1):
            c1=c1+k[0][i]
            c2=c2+k[nEle][i]
        q1_eqn = c1*bc1-f[0]+q1
        q2_eqn = c2*bc2-f[nEle]-q2
    elif method=='Quadratic':
        for i in range(2*nEle+1):
            c1=c1+k[0][i]
            c2=c2+k[nEle][i]
        q1_eqn = c1*bc1-f[0]+q1
        q2_eqn = c2*bc2-f[2*nEle]-q2
    if len(bcs)==0:
        return(solve(q1_eqn,q1),solve(q2_eqn,q2))
    elif len(bcs)==2:
        return(solve(q1_eqn,q1)[0].subs(bc1,bcs[0]),solve(q2_eqn,q2)[0].subs(bc2,bcs[1]))

# user inputs
domainLen=1
# number of elements
nEle=3
# problem data
a=1
c=-1
f=-x*x
method='Both' # accepts 'Quadratic','linear','Both'
# dirchlet boundary conditions values at left and right end
bcs=[2,3] # assumed boundary values [left end,right end]

if method=='Quadratic':
    kq,fq=kfmatixQad(nEle,domainLen,a,c,f)
    q1q,q2q=solQ(kq,fq,nEle,bcs,'Quadratic')
    fq[0]=fq[0]+q1q
    fq[len(fq)-1]=fq[len(fq)-1]+q2q
    sq=linalg.inv(kq).dot(fq)
    print('Quadratic Solution, U : \n',sq)

elif method=='linear':
    k,f=kfmatixLin(nEle,domainLen,a,c,f)
    q1,q2 = solQ(k,f,nEle,bcs,'linear')
    f[0]=f[0]+q1
    f[nEle]=f[nEle]+q2
    sl=linalg.inv(k).dot(f)
    print('Linear Solution, U : \n',sl)

elif method=='Both':
    kq,fq=kfmatixQad(nEle,domainLen,a,c,f)
    q1q,q2q=solQ(kq,fq,nEle,bcs,'Quadratic')
    fq[0]=fq[0]+q1q
    fq[len(fq)-1]=fq[len(fq)-1]+q2q
    sq=linalg.inv(kq).dot(fq)
    print('Quadratic Solution, U : \n',sq)
    k,f=kfmatixLin(nEle,domainLen,a,c,f)
    q1,q2 = solQ(k,f,nEle,bcs,'linear')
    f[0]=f[0]+q1
    f[nEle]=f[nEle]+q2
    sl=linalg.inv(k).dot(f)
    print('\nLinear Solution, U : \n',sl)

import numpy,from numpy,from scipy,from sympy


import numpy as np
from scipy.optimize import leastsq

def gaussian_func(x, args):
    a, b, c, d = args
    return a * np.exp(-(x - b) ** 2 / (2 * c ** 2)) + d

def gaussian_fit_init_args(points):
    xs, ys = np.array(points).transpose()
    dx = xs[1] - xs[0]
    peak_index = np.argmax(ys)
    a = ys[peak_index]
    b = xs[peak_index]
    d = np.min(ys)
    c = dx * np.sum(ys - d) / np.sqrt(2.0 * np.pi) / a
    return np.array((a, b, c, d), np.float)

def gaussian_fit(points):
    init_args = gaussian_fit_init_args(points)
    result = leastsq(
        lambda args, xs, ys: gaussian_func(xs, args) - ys,
        init_args, (points[:, 0], points[:, 1]))
    return result[0]

def gaussian_fwhm(args):
    return 2 * args[2] * np.sqrt(2 * np.log(2))


"""
Designed to calculate the linear regression of any data passed in.
Must take in at least two sets of data (x, y) axis in order for a
linear regression line to be calculated.

Author: Ashton Foulger
Version: 0.01, 11/3/21
Python-Version: 3.10.0
"""

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

"""
Takes in two sets of data to compute the sum of X times Y
variables from both sets of data. Returning an int value of
the sum of the two data arrays.
"""
def Compute_SumXY(data1, data2):
    data_XY = []

    for n1, n2 in zip(data1, data2):
        data_XY.append(n1 * n2)

    return sum(data_XY)

"""
Takes in a data set, an alpha and beta values to compute points
on the linear regression line. To then be used to plot the line of
regression.
"""
def Compute_Regression_Slope_Points(data, a, b):
    y = 0
    squarePoints = []

    for i in range(len(data)):
        y = a + b * data[i]
        squarePoints.append(y)

    return squarePoints

"""
Computes the Sum of Squares Error used in calculating the
coefficient of determination. Taking in a data array, points
(found on line of regression), and an n value to act as a count
for how may points we need to sum. Returning an int value of the sum
of squares error.
"""
def Compute_SSE(data, squares, n):
    temp = []

    for i in range(n):
        data[i] - squares[i]
        temp.append((data[i] - squares[i])**2)

    return sum(temp)

"""
Computes the Sum of Squares Total used in calculating the coefficent
of determination. Taking in a data array, Mean of Y axis points, and an
n value to act as a count for how many points we need to sum. Returning
an int value of the sum of squares total.
"""
def Compute_SST(Y_data, Y_mean, n):
    temp = []
    for i in range(n):
        temp.append((Y_data[i] - Y_mean)**2)

    return sum(temp)

"""
Computes the Coefficent of Determination for linear regression line from the data.
Taking in a Sum of Squares Error int value, and Sum of Squares Total in value.
Returning an int value of R squared.
"""
def Compute_R_Squared(SSE, SST):
    R = 1 - (SSE / SST)
    return R

#---------------LINE_OF_BEST_FIT_CALCULATION---------------#

#Data Arrays
data_X = [1,2,3,4,5,6]
data_Y = [0,7,6,14,11,10]

#Data Sample Means
mean_X = np.mean(data_X)
mean_Y = np.mean(data_Y)

#Data Sample Variances
variance_X = np.var(data_X)
variance_Y = np.var(data_Y)

#Data Linear Regression Equation Variables
n = 6
sumX = sum(data_X)
sumXX = sum(np.power(data_X, 2))
sumY = sum(data_Y)
sumXY = Compute_SumXY(data_X, data_Y)
a = ((sumY * sumXX) - (sumX * sumXY)) / ((n * sumXX) - (sumX)**2)
b = ((n * sumXY) - (sumX * sumY)) / ((n * sumXX) - (sumX)**2)

#Data Coefficent of Determination Variable
lineOfBestFitSquares = Compute_Regression_Slope_Points(data_X, a, b)
SSE = Compute_SSE(data_Y, lineOfBestFitSquares, n)
SST = Compute_SST(data_Y, mean_Y, n)
R = Compute_R_Squared(SSE, SST)

#Print Linear Regression Values & Coefficent of Determination
print("\n")
print("Mean X : " + str(mean_X))
print("Mean Y : " + str(mean_Y))
print("Variance X : " + str(round(variance_X , 2)))
print("Variance Y : " + str(round(variance_Y, 2)))
print("Sum X : " + str(sumX))
print("Sum XX : " + str(sumXX))
print("Sum Y : " + str(sumY))
print("Sum XY : " + str(sumXY))
print("Var a : " + str(a))
print("Var b ; " + str(b))
print("SSE : " + str(SSE))
print("SST : " + str(SST))
print("R^2 : " + str(R))
print("\n")

#Display Linear Regression Graph
plt.scatter(data_X,data_Y, marker = 'd', color = 'blue')
plt.plot(data_X, data_Y, color = 'blue')
plt.scatter(data_X, lineOfBestFitSquares, marker = 's', color = 'red')
plt.plot(data_X, lineOfBestFitSquares, color = 'red')
plt.show()


import numpy as np
from functools import partial
from pyspark import SparkContext
from pyspark.sql import SparkSession

sc = SparkSession.builder.appName("AdvDat").getOrCreate()

def haversine(p,q):
    """
    Calculate haversine distance based on coordinates p,q
    """
    lng1, lat1 = p
    lng2, lat2 = q
    dlng = abs(lng2-lng1)
    dlat = abs(lat2-lat1)

    a = np.sin(0.5*dlng)**2 + np.cos(lng1)*np.cos(lng2)*np.sin(0.5*dlat)**2
    c = 2 * np.arctan2(np.sqrt(a),np.sqrt(1-a))
    return c*6371 # earth radius

def coordinates(line):
    """
    Produces coordinates tuples
    line: CSV string from RDD
    """
    contents = line.split(",")
    lng, lat = map(float,contents[3:5])
    return lng, lat

def closest(centroids,coordinates):
    """
    Computes the minimum haversine distance and saves centroid
    centroids: contains the centroids as tuples (id,coordinates)
    coordinates: tuple of floats

    Returns id_coords: tuple (int,tuple)
    Tuple containing the id of the closest centroid
    and the tuple of coordinates of this point
    """
    tup = [(cen[0], haversine(coordinates,cen[1])) for cen in centroids]
    distance = min(tup, key = lambda x:x[1])
    return (distance[0],coordinates)

def sum_by_elem(p,q):
    """
    Reduce Function, sums each coordinate of 2 items
    p,q: tuples of (tuple of floats: coordinates,int)
    Returns tuple of (tuple of summed floats, summed int)
    """
    p, num1 = p
    q, num2 = q
    tup = map(sum,zip(p,q))
    return (tuple(tup),num1+num2)

def avg_by_elem(p):
    """
    Averages the coordinates assigned to centroid
    p: tuple returned by sum_by_elem
    Returns tuple of floats: mean of coordinates
    """
    p, num = p
    avg = map(lambda x:x/num, p)
    return tuple(avg)

data = sc.textFile('yellow_tripdata_1m.csv')
# get all coordinates as tuples and filter outliers
population = data.map(coordinates).filter(lambda line: line[0] and line[1]).cache()

# take first 5 as initial centroids with id >= 1
centroids = list(enumerate(population.take(5),1))

for _ in range(3):
    # assign current centroids to closest() function
    p_closest = partial(closest,centroids)
    # find closest centroid for each pair of coordinates
    cen_coords = population.map(p_closest)
    # find new centroids by map-reduce-map
    new_centroids = cen_coords.mapValues(lambda label:(label,1))\
                              .reduceByKey(sum_by_elem)\
                              .mapValues(avg_by_elem)
    centroids = new_centroids.collect()

# save results in csv file
centroids = new_centroids.toDF(["id","centroid"])
centroids.withColumn("centroid", centroids.centroid.cast("string"))\
         .coalesce(1).write\
         .csv("/Centroids",header="true",mode="overwrite",quote="")


from numpy import zeros, array

def gauss_seidel(A,b,x10,x20,x30,n):
    x1 = zeros(n)
    x2 = zeros(n)
    x3 = zeros(n)
    x1[0] = x10
    x2[0] = x20
    x3[0] = x30
    print('%7s %9s %9s %9s' % ('k','x1','x2','x3'))
    print('%7d %9.5f %9.5f %9.5f' % (0,x1[0],x2[0],x3[0]))
    for k in range(n-1):
        x1[k+1] = (b[0] - A[0,1] * x2[k] - A[0,2] * x3[k])/A[0,0]
        x2[k+1] = (b[1] - A[1,0] * x1[k+1] - A[1,2] * x3[k])/A[1,1]
        x3[k+1] = (b[2] - A[2,0] * x1[k+1] - A[2,1] * x2[k+1])/A[2,2]
        print('%7d %9.5f %9.5f %9.5f' % (k+1,x1[k+1],x2[k+1],x3[k+1]))
    return x1,x2,x3

A = array([[5,-1,1],[-1,3,-1],[1,-1,4]])
b = array([6,2,11])
x10 = 0.0
x20 = 0.0
x30 = 0.0
n = 10
x1, x2, x3 = gauss_seidel(A,b,x10,x20,x30,n)


import numpy as np
from math import sqrt

def RMSE(y,y_hat):
    '''
    args
    ----
        y: true values (np array)
        y_hat: predicted values (np array)

     returns
     ----
        the root mean swquared error

    '''
    return sqrt(np.mean(np.square(y-y_hat)))


# Author: OMKAR PATHAK

import numpy as np

angles = np.array([0, 30, 45, 60, 90, 180, 360])

# Convert to radians by multiplying with pi/180
# for getting sine of angles
print(np.sin(angles * np.pi/180))

# for getting cosine of angles
print(np.cos(angles * np.pi/180))

# for getting tangent of angles
print(np.tan(angles * np.pi/180))

# for computing inverse of trigonometric functions
sine = np.sin(angles * np.pi/180)
sineinv = np.arcsin(sine)
# computing angle from inverse
print(np.degrees(sineinv))

# for rounding the values
print(np.around(sine, 4))       # [ 0.      0.5     0.7071  0.866   1.      0.     -0.    ]

# for rounding to previous integer
print(np.floor(sine))           # [ 0.  0.  0.  0.  1.  0. -1.]

# for rounding to next integer
print(np.ceil(sine))            # [ 0.  1.  1.  1.  1.  1. -0.]


# %% [markdown]
# # Gradient filters
# [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/YoniChechik/AI_is_Math/blob/master/c_03_edge_detection/gradient_filters.ipynb)

# %%
# to run in google colab
import sys
if 'google.colab' in sys.modules:
    import subprocess
    subprocess.call('apt-get install subversion'.split())
    subprocess.call('svn export https://github.com/YoniChechik/AI_is_Math/trunk/c_03_edge_detection/Bikesgray.jpg'.split())

# %%
import numpy as np
import cv2
from matplotlib import pyplot as plt

figsize = (10,10)

# %% [markdown]
# ## Original image
# %%
img = cv2.imread("Bikesgray.jpg")
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
plt.figure(figsize=figsize)
plt.imshow(img, cmap='gray', vmin=0, vmax=255)
plt.title('Original image')

# %% [markdown]
# ## X absolute grad filter
# %%
img = img.astype(float)  # 'uint8' doesn't work with minus sign - for filtering

# 1. cv2.filter2D is working with corelation rether than convolution
#    no need to flip the kernel
# 2. Notice that kernel is 2D array - if 1d than we will get a column vector convolution
kernel = np.array([[-1, 0, +1]])
dst = cv2.filter2D(img, -1, kernel)

plt.figure(figsize=figsize)
plt.imshow(np.abs(dst), cmap='gray')
plt.colorbar()
plt.title('$f\'_x$: image filtered with '+str(kernel))

print("kernel shape is "+str(kernel.shape))

# %% [markdown]
# ## Y grad filter (no abs)
# %%
kernel = np.array([[-1, 0, +1]]).T
dst = cv2.filter2D(img, -1, kernel)

plt.figure(figsize=figsize)
plt.imshow(dst, cmap='gray')
plt.colorbar()
plt.title('$f\'_y$: image filtered with\n '+str(kernel))

# %% [markdown]
# ## Comparison of x gradient filters
# %%
plt.rcParams['figure.figsize'] = [20, 20]

plt.subplot(4, 2, 1)
plt.imshow(img, cmap='gray', vmin=0, vmax=255)
plt.title('original image')

#######################################
kernel = 1/2*np.array([[-1, 0, +1]])
dst_sym = cv2.filter2D(img, -1, kernel)

plt.subplot(4, 2, 2)
plt.imshow(dst_sym, cmap='gray')
plt.title('$f\'_x$: image filtered with symmetric derivative')

#######################################
kernel = 1/6*np.array([
    [-1, 0, +1],
    [-1, 0, +1],
    [-1, 0, +1]])
dst_prewitt = cv2.filter2D(img, -1, kernel)

plt.subplot(4, 2, 3)
plt.imshow(dst_prewitt, cmap='gray')
plt.title('$f\'_x$: image filtered with Prewitt')

#######################################
# cv2.Sobel() also exist
kernel = 1/8*np.array([
    [-1, 0, +1],
    [-2, 0, +2],
    [-1, 0, +1]])
dst_sobel = cv2.filter2D(img, -1, kernel)

plt.subplot(4, 2, 4)
plt.imshow(dst_sobel, cmap='gray')
plt.title('$f\'_x$: image filtered with Sobel')

#######################################
dst_cv2_sobel = cv2.Sobel(img, -1, 1, 0)  #cv2.Sobel(img,ddepth,x_size,y_size)

plt.subplot(4, 2, 5)
plt.imshow(dst_cv2_sobel, cmap='gray')
plt.colorbar()
plt.title('cv2.Sobel X')

#######################################
plt.subplot(4, 2, 6)
plt.imshow(np.abs(dst_sobel-dst_sym))
plt.colorbar()
plt.title('|sobel-symmetric|')

#######################################
plt.subplot(4, 2, 7)
plt.imshow(np.abs(dst_sobel-dst_prewitt))
plt.colorbar()
plt.title('|sobel-prewitt|')

#######################################
plt.subplot(4, 2, 8)
plt.imshow(np.abs(dst_sym-dst_prewitt))
plt.colorbar()
plt.title('|symmetric-prewitt|')
# %% [markdown]
# ## Filtering common errors
# ### kernel dimension error
# Note that you need to use a 2D array for a horizontal kernel!
# %%

kernel = np.array([-1, 0, +1])
dst = cv2.filter2D(img, -1, kernel)

plt.figure(figsize=figsize)
plt.imshow(np.abs(dst), cmap='gray')
plt.colorbar()
plt.title('wrong kernel dim: '+str(kernel))

print("kernel shape is "+str(kernel.shape))

# %% [markdown]
# ### uint8 errors
# Wrong filtering when keeping uint8 instead of float, because uint8 doesn't have negative numbers...
# %%
uint8_img = np.zeros((500, 500), dtype=np.uint8)
uint8_img[200:300, 200:300] = 1

kernel = np.array([[-1, 0, +1]])
dst = cv2.filter2D(uint8_img, -1, kernel)

fig, axs = plt.subplots(1, 2, figsize=(20,20))
axs[0].imshow(uint8_img, cmap='gray')
axs[0].title.set_text('original image')
axs[1].imshow(dst, cmap='gray')
axs[1].title.set_text('uint8 WRONG filtering')


import numpy
from numpy import sin, arange
import matplotlib.pyplot
from matplotlib.pyplot import *
from sympy import Limit

def g( x ):
    g = x ** 2
    return g

# An example will be used to show right and left convergence to a value.
# Convergence at x = 0 will be shown graphically using g(x).

n = 5  # This determines the number of values calculated on each side of x=0.

powers = arange( 0, n+1 )

denominator = 2.0 ** powers  # denominator contains exponentiated values of 2.0.

delta = 1           # This is the interval used on either side of the origin.

# The following are values of x and f(x) trending to the limit.
# Delta is being divided by powers of 2 to reduce the distance from the limit.
# The letter "r" denotes from the right, and "l" denotes from the left.

x_r = delta / denominator
y_r = g( x_r )
x_l = -x_r   # The negative sign generates a symmetric point on the left.
y_l = g( x_l )

# The following determines the vertical boundaries of the resulting plot.

ymax = max( abs( y_r ) ) + 0.5
ymin = -ymax

figure()
xlim( -delta - 0.5 , delta + 0.5 )
ylim( ymin, ymax )

# Plotting is being done in layers.  First the line plot then the points.

plot( x_r,y_r, color = 'b' )
plot( x_l,y_l, color = 'r' )

# The black points were computed.  The yellow point marks the limit.

scatter( x_r,y_r, color = 'k', s= 30 )
scatter( x_l,y_l, color = 'k', s= 30 )
scatter( 0.0, g(0.0), c = 'y', s = 40 )

title( 'Example of Convergence to a Functional Value' )
xlabel( 'x-axis' )
ylabel( 'y-axis' )
show()


""" Utilities """

import torch
import numpy as np
import random

def set_seed(seed=None):
	"""Set random seed, or reset if None.
	"""
	random.seed(seed)
	np.random.seed(seed)
	if seed is None:
		torch.manual_seed(random.randint(1,1e6))
	else:
		torch.manual_seed(seed)

def spectral_radius(A: torch.Tensor, eps=None, n_iter=1000):
	"""Differentiable procedure for computing spectral radius (magnitude of largest eigenvalue)

	Args:
		A: square matrix
		eps: (optional) if provided, uses convergence-based power iteration method
		n_iter: (optional) if provided, uses power iterations with this many steps (default; 1000 steps)

	For 2x2 systems, performs direct computation.
	"""
	if A.shape[0] == A.shape[1] == 2:
		tr, det = A.trace(), A.det()
		disc = tr**2 - 4*det
		if disc >= 0:
			return torch.max(
				torch.abs(tr + torch.sqrt(disc)) / 2,
				torch.abs(tr - torch.sqrt(disc)) / 2
			)
		else:
			return torch.sqrt((tr/2)**2 - disc/4)
	elif eps is not None:
		return _sp_radius_conv(A, eps)
	else:
		return _sp_radius_niter(A, n_iter)

def _sp_radius_conv(A: torch.Tensor, eps: float):
	v = torch.ones((A.shape[0], 1), device=A.device)
	v_new = v.clone()
	ev = v.t()@A@v
	ev_new = ev.clone()
	while (A@v_new - ev_new*v_new).norm() > eps:
		v = v_new
		ev = ev_new
		v_new = A@v
		v_new = v_new / v_new.norm()
		ev_new = v_new.t()@A@v_new
	return ev_new

def _sp_radius_niter(A: torch.Tensor, n_iter: int):
	v = torch.ones((A.shape[0], 1), device=A.device)
	for _ in range(n_iter):
		v = A@v
		v = v / v.norm()
	ev = v.t()@A@v
	return ev

def is_semistable(P: torch.Tensor, eps=1e-2):
	"""Semistability test for transfer operator using spectral radius """
	return spectral_radius(P).item() <= 1.0 + eps

def rmse(X: torch.Tensor, Y: torch.Tensor):
	assert X.shape == Y.shape
	return torch.sqrt(torch.mean((X - Y)**2)).item()

""" Tests """

if __name__ == '__main__':
	set_seed(9001)
	device = 'cuda' if torch.cuda.is_available() else 'cpu'

	prec = 1e-2

	# 2d spectral radius test
	for _ in range(100):
		d = 2
		A = torch.randn((d, d), device=device)
		e = np.random.uniform(0.1, 2.00)
		L = torch.linspace(e, 0.01, d, device=device)
		P = torch.mm(torch.mm(A, torch.diag(L)), torch.pinverse(A))

		np_e_max = np.abs(np.linalg.eigvals(P.cpu().numpy())).max()
		pwr_e_max = spectral_radius(P).item()
		print('True:', e, 'numpy:', np_e_max, 'pwr_iter:', pwr_e_max)
		assert np.abs(e - pwr_e_max) < prec

	# Nd Power iteration test
	for _ in range(100):
		d = 100
		A = torch.randn((d, d), device=device)
		e = np.random.uniform(0.1, 1.10)
		L = torch.linspace(e, 0.01, d, device=device)
		P = torch.mm(torch.mm(A, torch.diag(L)), torch.pinverse(A))

		np_e_max = np.abs(np.linalg.eigvals(P.cpu().numpy())).max()
		pwr_e_max = spectral_radius(P).item()
		print('True:', e, 'numpy:', np_e_max, 'pwr_iter:', pwr_e_max)
		assert np.abs(e - pwr_e_max) < prec


"""

 logistic_regression_GD.py  (author: Anson Wong / git: ankonzoid)

"""
import os, random
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.metrics import log_loss

def sigmoid(z):
    return 1.0 / (1.0 + np.exp(-z))

def split(fracs, N, seed):
    def is_intersect(arr1, arr2):
        n_intersect = len(np.intersect1d(arr1, arr2))
        if n_intersect == 0: return False
        else: return True
    fracs = [round(frac, 2) for frac in fracs]
    if sum(fracs) != 1.00:
        raise Exception("fracs do not sum to one!")
    indices = list(range(N))
    random.Random(seed).shuffle(indices)
    indices = np.array(indices, dtype=int)
    n_fracs = []
    for i in range(len(fracs) - 1):
        n_fracs.append(int(max(fracs[i] * N, 0)))
    n_fracs.append(int(max(N - sum(n_fracs), 0)))
    if sum(n_fracs) != N:
        raise Exception("n_fracs do not sum to N!")
    n_selected = 0
    indices_fracs = []
    for n_frac in n_fracs:
        indices_frac = indices[n_selected:n_selected + n_frac]
        indices_fracs.append(indices_frac)
        n_selected += n_frac
    for a, indices_frac_A in enumerate(indices_fracs):
        for b, indices_frac_B in enumerate(indices_fracs):
            if a == b:
                continue
            if is_intersect(indices_frac_A, indices_frac_B):
                raise Exception("there are intersections!")
    return indices_fracs

class LogisticRegressorGD():

    def __init__(self):
        pass

    def fit(self, X, y, X_tune, y_tune):
        #
        # Use binary cross-entropy loss instead of MSE
        #
        # L = -(1/N) * sum[i=1, N] (y*log(y_hat) + (1-y)*log(1-y_hat))
        #   = -(1/N) * sum[i=1, N] (y*log(sigma(a*x + a0)) + (1-y)*log(1-sigma(a*x + a0)))
        #
        # y_hat = sigma(x)
        #
        # dL/daj = -(1/N) * sum[i=1, N] * y * xj for j=1,2,...,d
        # dL/daj = -(1/N) * sum[i=1, N] * y for j=0
        #
        # GD: a{i+1} <- a{i} - \eta * dL/dxj
        #
        print("Fitting...")

        # Initialize fit parameters [a0, a1, a2, ..., ad] where a0 is y-intercept
        N, d = X.shape
        a_fit = np.random.normal(size=d+1)

        # Make gradient descent updates
        loss_tolerance = 1E-4
        fit_tolerance = 1E-4
        eta = 10  # learning rate
        converge = False
        loss = 9E99
        while not converge:

            # Compute gradient
            gradient = np.zeros(d+1)
            for i in range(N):
                y_pred_i = sigmoid(a_fit[0] + np.dot(a_fit[1:], X[i]))
                gradient[0] += (y_pred_i - y[i])
                gradient[1:] += (y_pred_i - y[i]) * X[i, 0:]
            gradient /= N

            # Perform gradient descent step
            a_fit_new = a_fit - eta * gradient

            # Compute loss (to keep track)
            y_pred = np.array([sigmoid(a_fit_new[0] + np.dot(a_fit_new[1:], x)) for x in X_tune])
            loss_new = log_loss(y_tune, y_pred)
            print("loss = {}".format(loss))

            # See if loss and fit parameters have converged
            if np.abs(loss_new - loss) < loss_tolerance and loss_new > loss_tolerance:
                converge = True

            # Update fit parameters
            a_fit = a_fit_new
            loss = loss_new

        # Save fit parameters
        self.a_fit = a_fit

    def predict(self, X):
        y_pred = np.array([sigmoid(self.a_fit[0] + np.dot(self.a_fit[1:], x)) for x in X])
        return y_pred

def plot_sorted(X_train, y_pred_train, X_valid, y_pred_valid):
    idx_train_sorted = np.argsort(X_train.flatten())
    idx_valid_sorted = np.argsort(X_valid.flatten())
    plt.plot(X_train[idx_train_sorted].flatten(), y_pred_train[idx_train_sorted], 'r--')
    plt.plot(X_valid[idx_valid_sorted].flatten(), y_pred_valid[idx_valid_sorted], 'k-')

# Main Driver
if __name__ == "__main__":

    random.seed(10)
    N0, N1 = 400, 400
    N = N0 + N1
    X = np.array([random.uniform(0, 0.6) for _ in range(N0)] + [random.uniform(0.4, 1) for _ in range(N1)]).reshape(-1, 1)
    y = np.array([0 for _ in range(N0)] + [1 for _ in range(N1)], dtype=int)
    assert len(X) == len(y)

    indices_valid, indices_train = split(fracs=[0.2, 0.8], N=N, seed=1)
    X_train, y_train = X[indices_train], y[indices_train]
    X_valid, y_valid = X[indices_valid], y[indices_valid]

    model = LogisticRegressorGD()
    model.fit(X, y, X_tune=X_valid, y_tune=y_valid)
    y_pred_train = model.predict(X_train)
    y_pred_valid = model.predict(X_valid)
    bce_train = log_loss(y_train, y_pred_train)
    bce_valid = log_loss(y_valid, y_pred_valid)
    print("bce_train =", bce_train)
    print("bce_valid =", bce_valid)

    plt.figure(1)
    plt.plot(X.flatten(), y, 'o')
    plot_sorted(X_train, y_pred_train, X_valid, y_pred_valid)
    plt.show()


#1-Dimensional Data:

#Import required libraries:
from scipy import stats

#Dataset:
d = [1,2,3,4,5]

#Finding 0th moment:
print("0th Moment = ",stats.moment(d,moment=0))

#Finding 1st moment:
print("1st Moment = ",stats.moment(d,moment=1))

#Finding 2nd moment:
print("2nd Moment = ",stats.moment(d,moment=2))

#Finding 3nd moment:
print("3nd Moment = ",stats.moment(d,moment=3))

#Finding 4th moment:
print("4th Moment = ",stats.moment(d,moment=4))

#============================================================

#2-Dimensional Data:

#Import required libraries:
from scipy import stats

#Dataset:
d = [[5,6,9,11,3],[21,4,8,15,2]]

#Finding 0th moment:
print("0th Moment = ",stats.moment(d,moment=0))

#Finding 1st moment:
print("1st Moment = ",stats.moment(d,moment=1))

#Finding 2nd moment:
print("2nd Moment = ",stats.moment(d,moment=2))

#Finding 3nd moment:
print("3nd Moment = ",stats.moment(d,moment=3))

#Finding 4th moment:
print("4th Moment = ",stats.moment(d,moment=4))

#============================================================

#2-Dimensional Data:
#Set axis=1 (Horizonatal):

#Import required libraries:
from scipy import stats

#Dataset:
d = [[5,6,9,11,3],[21,4,8,15,2]]

#Finding 0th moment:
print("0th Moment = ",stats.moment(d,moment=0,axis=1))

#Finding 1st moment:
print("1st Moment = ",stats.moment(d,moment=1,axis=1))

#Finding 2nd moment:
print("2nd Moment = ",stats.moment(d,moment=2,axis=1))

#Finding 3nd moment:
print("3nd Moment = ",stats.moment(d,moment=3,axis=1))

#Finding 4th moment:
print("4th Moment = ",stats.moment(d,moment=4,axis=1))

#============================================================

#Multi-Dimensional Data:

#Import required libraries:
from scipy import stats

#Dataset:
d = [[5,6,9,11,3],
     [21,4,8,15,2],
    [15,23,42,1,36]]

#Finding 0th moment:
print("0th Moment = ",stats.moment(d,moment=0))

#Finding 1st moment:
print("1st Moment = ",stats.moment(d,moment=1))

#Finding 2nd moment:
print("2nd Moment = ",stats.moment(d,moment=2))

#Finding 3nd moment:
print("3nd Moment = ",stats.moment(d,moment=3))

#Finding 4th moment:
print("4th Moment = ",stats.moment(d,moment=4))

#============================================================

#2-Dimensional Data:
#Set axis=1 (Horizonatal):
#Higher Order Moments:

#Import required libraries:
from scipy import stats

#Dataset:
d = [[5,6,9,11,3],[21,4,8,15,2]]

#Finding 10th moment:
print("10th Moment = ",stats.moment(d,moment=10,axis=1))

#Finding 12th moment:
print("12th Moment = ",stats.moment(d,moment=12,axis=1))


#################################################################
# Name:     Gauss-Seidel.py                                     #
# Authors:  Michael Battaglia                                   #
# Function: Program calculates PDE solution using Gauss-Seidel  #
#           relaxation method.                                  #
#################################################################

#essential imports
import numpy as np
import matplotlib.pyplot as plt

#function: Gauss-Seidel relaxation method
def GaussSeidel(image, delta, relax, flag=-99, plot=False):
    #assume image full of flags except at boundaries
    im1 = np.copy(image)
    #calculate boundary mean
    bdy = []
    for i in range(image.shape[0]):
        for j in range(image.shape[1]):
            if abs(image[i][j]-flag) > 1e-10:
                #point is on boundary
                bdy.append(image[i][j])
    bdy = np.mean(bdy)
    #change flags to boundary mean
    for i in range(image.shape[0]):
        for j in range(image.shape[1]):
            if abs(image[i][j]-flag) < 1e-10:
                #point is in interior
                im1[i][j] = bdy
    #plot initial state if parameter give
    if plot:
        plt.imshow(im1.T, interpolation='nearest', cmap='gray')
        plt.title("Gauss-Seidel Solving PDE")
        plt.colorbar()
        plt.draw()
        plt.pause(0.01)

    #initialize error
    error = 2*delta
    #relax until error within tolerance
    while error > delta:
        im2 = np.copy(im1)
        #for each row
        for i in range(image.shape[0]):
            #for each column
            for j in range(image.shape[1]):
                #leave boundary untouched (mark non-bnd by -99)
                if abs(image[i][j]-flag) < 1e-10:
                    #Gauss Seidel relaxation
                    im2[i][j] = 0.25*(1+relax)*(im2[i+1,j]+im2[i-1,j]
                                    +im2[i,j+1]+im2[i,j-1])-relax*im2[i][j]
        #calculate error
        error = np.amax(np.absolute(im2-im1))
        im1 = np.copy(im2)
        #plot if parameter give
        if plot:
            plt.cla()
            plt.imshow(im1.T, interpolation='nearest', cmap='gray')
            plt.draw()
            plt.pause(0.01)
    #close plots
    if plot:
        plt.close()
    return im1, error

#function: main
if __name__ == '__main__':
    #construct grid 8cm x 20cm at 0.1cm resolution
    length = 8.0
    width = 20.0
    dx = 0.1
    T = -99*np.ones([int(width/dx),int(length/dx)],float)
    #initialize boundary values
    T[:,0].fill(10.0)  #top row
    T[0,:] = np.linspace(10.0,0,T.shape[1],endpoint=True) #left column
    T[-1,:] = np.linspace(10.0,0,T.shape[1],endpoint=True) #right column
    #bottom row with indent
    T[0:int(5.0/dx),-1] = np.linspace(0,5.0,int(5.0/dx),endpoint=True) #lrow
    T[T.shape[0]-int(5.0/dx):T.shape[0],-1] = np.linspace(5.0,0,int(5.0/dx),endpoint=True) #rrow
    T[int(5.0/dx)-1,T.shape[1]-int(3.0/dx):T.shape[1]] = np.linspace(7.0,5.0,int(3.0/dx),endpoint=True) #left indent
    T[T.shape[0]-int(5.0/dx),T.shape[1]-int(3.0/dx):T.shape[1]] = np.linspace(7.0,5.0,int(3.0/dx),endpoint=True) #right indent
    T[int(5.0/dx)-1:T.shape[0]-int(5.0/dx)+1,T.shape[1]-int(3.0/dx)].fill(7.0) #top indent
    #fill out indent area
    T[int(5.0/dx):T.shape[0]-int(5.0/dx),T.shape[1]-int(3.0/dx)+1:T.shape[1]].fill(0.0)

    #PDE solution accuracy
    delta = 1.0e-6
    #Gauss-Seidel relaxation parameter w
    relax = 0.9
    #solve PDE using Gauss-Seidel relaxation
    T,error = GaussSeidel(T, delta=delta, relax=relax, flag=-99, plot=True)

    #plot final solution
    plt.title("Gauss-Seidel Relaxation for Laplace's Eqn")
    plt.imshow(T.T, interpolation='nearest', cmap='gray')
    plt.colorbar()
    plt.show()


# -*- coding: utf-8 -*-

import numpy as np

def estimate_kaiser_bessel_beta(W):
    r"""
    Estimate the kaiser bessel beta using the following heuristic:

    .. math::

        \beta = 2.34 \times W

    Derived from `Nonuniform fast Fourier transforms
    using min-max interpolation
    <nufft-min-max-ref_>`_.

    .. _nufft-min-max-ref: https://ieeexplore.ieee.org/document/1166689/

    Parameters
    ----------
    W : int
        Width of the filter

    Returns
    -------
    float
        Kaiser Bessel beta shape parameter
    """
    return 2.34*W

def kaiser_bessel(u, W, beta):
    r"""
    Compute a 1D Kaiser Bessel filter as defined
    in `Selection of a Convolution Function
    for Fourier Inversion Using Gridding
    <kbref_>`_.

    .. _kbref: https://ieeexplore.ieee.org/document/97598/

    Parameters
    ----------
    u : :class:`numpy.ndarray`
        Filter positions
    W : int
        Width of the filter
    beta : float, optional
        Kaiser Bessel shape parameter

    Returns
    -------
    :class:`numpy.ndarray`
        Kaiser Bessel filter with the same shape as `u`
    """

    # Sanity check
    hW = W // 2
    assert np.all(-hW <= u) & np.all(u <= hW)

    param = 1 - (2 * u / W)**2
    param[param < 0] = 0  # Zero negative numbers
    return np.i0(beta * np.sqrt(param)) / np.i0(beta)

def kaiser_bessel_with_sinc(u, W, oversample, beta, normalise=True):
    """
    Produces a filter composed of Kaiser Bessel multiplied by a sinc.

    Accounts for the oversampling factor, as well as normalising the filter.

    Parameters
    ----------
    u : :class:`numpy.ndarray`
        Filter positions
    W : int
        Width of the filter
    oversample : int
        Oversampling factor
    beta : float
        Kaiser Bessel shape parameter
    normalise : optional, {True, False}
        True if the filter should be normalised

    Returns
    -------
    :class:`numpy.ndarray`
        Filter with the same shape as `u`
    """
    kb = kaiser_bessel(u, W, beta)
    kb *= oversample
    kb *= np.sinc(u / oversample)

    if normalise:
        kb /= np.trapz(kb, u)

    return kb

def kaiser_bessel_fourier(x, W, beta):
    r"""
    Computes the Fourier Transform of a 1D Kaiser Bessel filter.
    as defined in `Selection of a Convolution Function
    for Fourier Inversion Using Gridding
    <kbref_>`_.

    .. _kbref: https://ieeexplore.ieee.org/document/97598/

    Parameters
    ----------
    x : :class:`numpy.ndarray`
        Filter positions
    W : int
        Width of the filter.
    beta : float
        Kaiser bessel shape parameter

    Returns
    -------
    :class:`numpy.ndarray`
        Fourier Transform of the Kaiser Bessel,
        with the same shape as `x`.
    """
    term = (np.pi*W*x)**2 - beta**2
    val = np.lib.scimath.sqrt(term).real
    return np.sin(val)/val


import numpy as np

class AutoDiff():
    '''
    Class to use for automatic differentiation, forward mode

    Addresses the following need.

    1) The user has some function which maps some number of input variables, for example x and y, to a single
    output value, f(x,y).

    2) The user would like to calculate f(x,y) at a point (x,y) that they specify, as well as the partial
    derivatives df/dx and df/dy at that same point (x, y).

    By first defining their input variables as AutoDiff objects, and then using these objects in
    standard python calculations, including with numpy functions, the user is returned a AutoDiff object
    whose val attribute is the value of the function, and whose derv attribute is the derivative of the function.

    Which partial derivative is stored in the derv attribute of the final AutoDiff object depends on which
    input variable's derv attribute is initially set to 1, while the derv attributes of the other input variables
    are set to 0.

    Example:
    x = AutoDiff(3,1)
    y = AutoDiff(2,0)
    f = x**2 + y**2 + 4
    print(f.val, f.dev)
    (17, 6)
    '''

    def __init__(self, val, derv=1):
        self.val = float(val)
        self.derv = float(derv)

    # -----------Overloading Urnary functions ---------------------------------------------

    def __pos__(self):
        return AutoDiff(self.val, self.derv)

    def __neg__(self):
        return AutoDiff(-1*self.val, -1*self.derv)

    # -----------Add, Subtract, Multiple, Divide functions --------------------------------

    def __mul__(self, other):
        try:
            return AutoDiff(self.val * other.val, self.derv * other.val + other.derv*self.val)
        except:
            return AutoDiff(self.val * other, self.derv * other)

    def __rmul__(self, other):
        return AutoDiff(self.val * other, self.derv * other)

    def __truediv__(self, other):
        try:
            return AutoDiff(self.val/other.val, self.derv/other.val + ( -1*self.val/(other.val**2) ) *other.derv)
        except:
            return AutoDiff(self.val/other, self.derv/other)

    def __rtruediv__(self, other):
        return AutoDiff(other/self.val,  -1*other/(self.val**2) )

    def __add__(self, other):
        try:
            return AutoDiff(self.val + other.val, self.derv + other.derv)
        except:
            return AutoDiff(self.val + other, self.derv + 0)

    def __radd__(self, other):
        return AutoDiff(self.val + other, self.derv + 0)

    def __sub__(self, other):
        try:
            return AutoDiff(self.val - other.val, self.derv + -1*other.derv)
        except:
            return AutoDiff(self.val - other, self.derv + 0)

    def __rsub__(self, other):
        return AutoDiff(other - self.val, -1*self.derv + 0)

    # -----------Overloading Trigonometric functions ---------------------------------------------

    def sin(self):
        return AutoDiff(np.sin(self.val), np.cos(self.val)*self.derv)

    def cos(self):
        return AutoDiff(np.cos(self.val), -1*np.sin(self.val)*self.derv)

    def tan(self):
        return AutoDiff(np.tan(self.val), (1/(np.cos(self.val)**2))*self.derv)

    def arcsin(self):
        if (self.val < -1) or (self.val > 1):
            raise ValueError(' Input to arcsin() must be between -1 and 1 inclusive.')
        else:
            return AutoDiff(np.arcsin(self.val), 1/np.sqrt(1 - (self.val)**2)*self.derv)

    def arccos(self):
        if (self.val < -1) or (self.val > 1):
            raise ValueError(' Input to arccos() must be between -1 and 1 inclusive.')
        else:
            return AutoDiff(np.arccos(self.val), -1/np.sqrt(1 - (self.val)**2)*self.derv)

    def arctan(self):
        return AutoDiff(np.arctan(self.val), 1/(1 + (self.val)**2)*self.derv)

    # -----------Overloading Hyperbolic functions ---------------------------------------------

    def sinh(self):
        return AutoDiff(np.sinh(self.val), np.cosh(self.val)*self.derv)

    def cosh(self):
        return AutoDiff(np.cosh(self.val), np.sinh(self.val)*self.derv)

    def tanh(self):
        return AutoDiff(np.tanh(self.val), (1/np.cosh(self.val))**2*self.derv)

    def arcsinh(self):
        return AutoDiff(np.arcsinh(self.val), 1/np.sqrt(1 +(self.val)**2)*self.derv)

    def arccosh(self):
        return AutoDiff(np.arccosh(self.val), 1/np.sqrt((self.val)**2 - 1) *self.derv)

    def arctanh(self):
        return AutoDiff(np.arctanh(self.val), 1/(1 - (self.val)**2) *self.derv)

    # -----------Overloading Power functions ---------------------------------------------

    def __pow__(self, other):
        try:
            return AutoDiff(self.val**other.val, other.val*self.val**(other.val - 1)*self.derv  +  self.val**other.val*np.log(self.val)*other.derv )
        except:
            return AutoDiff(self.val**other, other*self.val**(other - 1)*self.derv)

    def __rpow__(self, other):
        return AutoDiff(other**self.val, other**self.val*np.log(other)*self.derv)

    def exp(self):
        return AutoDiff(np.exp(self.val), np.exp(self.val)*self.derv)

    # -----------Overloading log functions ---------------------------------------------

    def log(self):
        return AutoDiff(np.log(self.val), (1/self.val)*self.derv)

    def log10(self):
        return AutoDiff(np.log10(self.val), (1/(np.log(10)*self.val))*self.derv)

    def log2(self):
        return AutoDiff(np.log2(self.val), (1 / (np.log(2) * self.val)) * self.derv)

    # -----------Overloading the Square Root function ------------------------------------------

    def sqrt(self):
        if self.val < 0:
            print('negative number has been input to the sqaure root function')
            return 0
        else:
            return AutoDiff(np.sqrt(self.val), (1/(2*np.sqrt(self.val) )*self.derv) )

    #-----------Overloading the Comparison Operators >, <, >=, <=, ==. != -------------------------

    def __lt__(self, other):
        try:
            if self.val < other.val:
                return True
            else:
                return False
        except:
            if self.val < other:
                return True
            else:
                return False

    def __gt__(self, other):
        try:
            if self.val > other.val:
                return True
            else:
                return False
        except:
            if self.val > other:
                return True
            else:
                return False

    def __le__(self, other):
        try:
            if self.val <= other.val:
                return True
            else:
                return False
        except:
            if self.val <= other:
                return True
            else:
                return False

    def __ge__(self, other):
        try:
            if self.val >= other.val:
                return True
            else:
                return False
        except:
            if self.val >= other:
                return True
            else:
                return False

    def __eq__(self, other):
        try:
            if self.val == other.val:
                return True
            else:
                return False
        except:
            if self.val == other:
                return True
            else:
                return False

    def __ne__(self, other):
        try:
            if self.val != other.val:
                return True
            else:
                return False
        except:
            if self.val != other:
                return True
            else:
                return False

def logist(x):
    '''
    Function to evaluate the logistic function at the input x, and if the input is
    an AutoDiff object, then also the derivative of the logistic function with
    respect to x

    Inputs:
        x (either a python float object or AutoDiff object)

    Returns:
        If x is a python float object, it returns the value of the logistic function evaluated at x (float).
        If x is an AutoDiff object, it returns another AutoDiff object whose val attribute contains the
        logistic function evaluated at x, and whose derv attribute contains the derivative of the
        logistic function with respect to x evaluated at x.
    '''
    if isinstance(x, AutoDiff):
        return AutoDiff(1 / (1 + np.exp(-1 * (x.val))),
                        (1 / (1 + np.exp(-1 * (x.val)))) * (1 - (1 / (1 + np.exp(-1 * (x.val))))) * x.derv)
    else:
        return 1/(1+np.exp(-1*x))

def logN(x, N):
    '''
    Function to evaluate the logarithm of the input x using the base N, and if the input is
    an AutoDiff object, then also the derivative of the logarithm function with repsect to
    x at x

    Inputs:
        x (either a python float object or AutoDiff object)
        N (int) the base of the logarithm function

    Returns:
        If x is a python float object, it returns the value of the logarithm base N function evaluated at x (float).
        If x is an AutoDiff object, it returns another AutoDiff object whose val attribute contains the
        logarithm base N function  evaluated at x, and whose derv attribute contains the derivative of the
        logarithm base N function with respect to x evaluated at x.
    '''
    if isinstance(x, AutoDiff):
        return AutoDiff(np.log(x.val)/np.log(N), (1/(x.val*np.log(N)) *x.derv))
    else:
        return np.log(x)/np.log(N)


#!/usr/bin/env python3
import argparse

import numpy as np

import matplotlib.pyplot as plt
import sklearn.datasets

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--clusters", default=3, type=int, help="Number of clusters")
    parser.add_argument("--examples", default=200, type=int, help="Number of examples")
    parser.add_argument(
        "--iterations",
        default=20,
        type=int,
        help="Number of kmeans iterations to perfom",
    )
    parser.add_argument(
        "--plot", default=False, action="store_true", help="Plot the results"
    )
    parser.add_argument("--seed", default=42, type=int, help="Random seed")
    args = parser.parse_args()

    # Set random seed
    np.random.seed(args.seed)

    # Generate artificial data
    data, target = sklearn.datasets.make_blobs(
        n_samples=args.examples,
        centers=args.clusters,
        n_features=2,
        random_state=args.seed,
    )

    # Start by using the first `args.clusters` samples as the cluster representations.
    centers = data[: args.clusters].copy()

    # TODO: Run `args.iterations` of the kmeans algorithm, storing zero-based cluster assignment
    # to `clusters`.
    clusters = np.zeros(data.shape[0], dtype=np.int8)
    for i in range(args.iterations):
        for row in range(data.shape[0]):
            observation = data[row, :]
            clusters[row] = np.argmin(
                [np.sum((center - observation) ** 2) for center in centers]
            )
        for center_index in range(args.clusters):
            centers[center_index] = np.mean(data[clusters == center_index, :], axis=0)

    if args.plot:
        plt.gca().set_aspect(1)
        plt.scatter(data[:, 0], data[:, 1], c=clusters)
        plt.scatter(centers[:, 0], centers[:, 1], marker="P", s=200, c="#ff0000")
        plt.scatter(
            centers[:, 0], centers[:, 1], marker="P", s=50, c=range(args.clusters)
        )
        plt.show()

    # Print the zero-based cluster assignments, one input example per line
    for cluster in clusters:
        print(cluster)


print ("[0/5] Importing libraries")
import struct
import wave
import numpy as np
import matplotlib.pyplot as plt

#VARIABLES
frequency = 261.63
frame_rate = 44100
duration = 5
channels = 1
sample_width = 2
amplitude = 1

print ('[1/5] Creating the values for the waves')
vals_nor = []
vals_inv = []
vals_mer = []
nor = wave.open('waves/normals/'+str(frequency)+'.wav', 'wb')
inv = wave.open('waves/inverses/'+str(frequency)+'.wav', 'wb')
mer = wave.open('waves/merges/'+str(frequency)+'.wav', 'wb')
for i in range (int(frame_rate*duration)):
	vals_nor.append(amplitude*np.sin(2*np.pi*frequency*(i/frame_rate)))
	vals_inv.append(amplitude*np.sin(2*np.pi*frequency*(i/frame_rate))*(-1))
	vals_mer.append(vals_nor[i]+vals_inv[i])

print ("[2/5] Defining waves' parameters")
nor.setnchannels(channels)
inv.setnchannels(channels)
mer.setnchannels(channels)
nor.setsampwidth(sample_width)
inv.setsampwidth(sample_width)
mer.setsampwidth(sample_width)
nor.setframerate(frame_rate)
inv.setframerate(frame_rate)
mer.setframerate(frame_rate)
nor.setnframes(len(vals_nor))
inv.setnframes(len(vals_inv))
mer.setnframes(len(vals_mer))
nor.setcomptype('NONE', 'not compressed')
inv.setcomptype('NONE', 'not compressed')
mer.setcomptype('NONE', 'not compressed')

print ("[3/5] Exporting the waves' raws")
raw_nor = open('raws/normals/'+str(frequency)+'.txt', 'w+')
raw_inv = open('raws/inverses/'+str(frequency)+'.txt', 'w+')
raw_mer = open('raws/merges/'+str(frequency)+'.txt', 'w+')
for i in range (int(frame_rate*duration)):
	raw_nor.write(str(vals_nor[i])+'\n')
	raw_inv.write(str(vals_inv[i])+'\n')
	raw_mer.write(str(vals_mer[i])+'\n')
raw_nor.close()
raw_inv.close()
raw_mer.close()

print ("[4/5] Writing the wav files")
for i in range (int(frame_rate*duration)):
	nor.writeframes(struct.pack('h', int(vals_nor[i]*32767)))
	inv.writeframes(struct.pack('h', int(vals_inv[i]*32767)))
	mer.writeframes(struct.pack('h', int(vals_mer[i]*32767)))
nor.close()
inv.close()
mer.close()

print ("[5/5] Plotting")
x = np.linspace(0, duration, num=int(frame_rate*duration))
plt.plot(x, vals_nor[:], 'b')
plt.plot(x, vals_inv[:], 'r')
plt.plot(x, vals_mer[:], 'g')
plt.title('Wave representation')
plt.xlabel('Time (s)')
plt.ylabel('Amplitude')
plt.show()


from typing import Iterable

import numpy as np
import scipy.stats

def compute_correlation(X: Iterable[float],
                        Y: Iterable[float],
                        ):
    """
    Compute the Pearson correlation coefficient from a sequence of samples of a joint variables (X_i, Y_i)

    :param X: (Iterable[float]) List of samples drawn from the first variable
    :param Y: (Iterable[float]) List of samples drawn from the second variable

    :return: Pearson coefficient of the joint variable (X, Y)

    """

    # Normalize the two variables
    X_normalized = (X - np.mean(X))/np.std(X)
    Y_normalized = (Y - np.mean(Y))/np.std(Y)

    # Compute the covariance of the normalized variables
    r = np.mean(X_normalized * Y_normalized)

    return r

def compute_pearson_correlation_p_value(X: Iterable[float],
                                        Y: Iterable[float],
                                        n: int = 10000.,
                                        ):
    """
    Compute the Pearson correlation p-value by permutation

    :param X: (Iterable[float]) List of samples drawn from the first variable
    :param Y: (Iterable[float]) List of samples drawn from the second variable
    :param n: (int) Number of times we simulate a correlation coefficient by permutation

    :return: p-values of the Pearson correlation test by permutation

    """

    # Compute the correlation coefficient on (X, Y)
    r = compute_correlation(X, Y)

    # Initialize the list of values comparisons
    r_list = []

    # MC
    for i in range(n):

        # Randomly permute X and Y to make the draws independent
        X = np.random.permutation(X)
        Y = np.random.permutation(Y)

        # Compute correlation on permuted variables and compare with r
        r_list.append(float(compute_correlation(X, Y) > r))

    # Compute the p-value of the Pearson correlation test
    p_value = np.mean(r_list)

    return p_value

if __name__ == '__main__':
    X = np.random.randn(1000)
    Y = np.random.randn(1000)
    print(scipy.stats.pearsonr(X, Y))
    print(compute_correlation(X, Y))
    print(compute_pearson_correlation_p_value(X, Y))

    X = np.arange(1000)
    Y = 7*np.arange(1000) + 4
    print(scipy.stats.pearsonr(X, Y))
    print(compute_correlation(X, Y))
    print(compute_pearson_correlation_p_value(X, Y))


import numpy as np
import os
from os.path import dirname
import sys
import matplotlib.pyplot as plt
import tensorflow as tf

# hide Tensorflow GPU warnings
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'

def generate_dummy_data(is_display_graph=False):
    total = 100
    x = np.arange(total)                                  # x is an array of 100 items: [0, 1, 2, ..., 99]
    y = 2.0 * x + np.random.uniform(-30, 30, size=total)  # y = 2 * x + random number -> linear form
    dummy_data = np.stack([x, y], axis=1)

    # draw the graph of input data
    if is_display_graph:
        plt.plot(x, y, 'o', alpha=0.5)
        plt.title('Input data')
        plt.show()

    return dummy_data

# define flags
tf.app.flags.DEFINE_integer('training_epochs', 10000, 'Total training epochs')
tf.app.flags.DEFINE_integer('display_step', 1000, 'Step to display the loss value')
tf.app.flags.DEFINE_float('learning_rate', 1e-9, 'The learning rate')
# tf.app.flags.DEFINE_float('expected_loss_minimum', 1e-10, 'The expected loss minimum value')
FLAGS = tf.app.flags.FLAGS

# init data
data = generate_dummy_data(True)
losses = []

# init the hypothesis model
X = tf.placeholder(tf.float32, name='X')
Y = tf.placeholder(tf.float32, name='Y')
w = tf.Variable(0.0, name='weight')
b = tf.Variable(0.0, name='bias')
hypothesis = w * X + b

# minimize the loss value using gradient descent
loss = tf.reduce_sum(tf.squared_difference(hypothesis, Y)) / 2 * data.shape[0]
optimizer = tf.train.GradientDescentOptimizer(FLAGS.learning_rate)
train = optimizer.minimize(loss)
training_data = {X: data[:, 0], Y: data[:, 1]}

# calculate score of the model
unexplained_loss = tf.reduce_sum(tf.squared_difference(hypothesis, Y), name='unexplained_loss')
R_squared = tf.subtract(1.0, tf.divide(unexplained_loss, loss))

# running the computation in Tensorflow
with tf.Session() as session:
    session.run(tf.global_variables_initializer())

    # previous_loss = 0.0
    print('\nModel training...')
    for epoch in range(FLAGS.training_epochs):
        _train, _loss = session.run([train, loss], feed_dict=training_data)
        losses.append(_loss)

        if epoch % FLAGS.display_step == 0:
            print('Epoch={}, loss value={}'.format(epoch, _loss))

        # we will quit the training process if we have reached our expected loss minimum value
        # if np.abs(previous_loss - _loss) < FLAGS.expected_loss_minimum:
        #     break
        # previous_loss = _loss

    _w, _b, _r_squared = session.run([w, b, R_squared], feed_dict=training_data)
    print('\nFinished!!!')
    print('y = {0:.4f} * x + {1:.4f}'.format(_w, _b))
    print('score = {0:.4f}\n'.format(_r_squared))

    # export model graph to Tensorboard
    # you can run this command to see the graph: tensorboard --logdir="./tensorboard/simple-linear"
    writer = tf.summary.FileWriter(dirname(__file__) + 'tensorboard/simple-linear', session.graph)
    writer.close()

    # display the fitted line
    plt.plot(data[:, 0], data[:, 1], "o", alpha=0.5)
    plt.plot(data[:, 0], _w * data[:, 0] + _b, alpha=0.5, label='Fitted line')
    plt.legend()
    plt.title('After predicted')
    plt.show()

    # draw the loss diagram
    plt.title('The loss diagram after trained')
    plt.plot(losses[:])
    plt.show()


# Example 5.3: Estimating Spot and Roll Returns Using the Constant Returns Model
import numpy as np
import pandas as pd
# import matplotlib.pyplot as plt
import statsmodels.api as sm

# import statsmodels.tsa.stattools as ts
# import statsmodels.tsa.vector_ar.vecm as vm

df = pd.read_csv('inputDataDaily_C2_20120813.csv')
df['Date'] = pd.to_datetime(df['Date'], format='%Y%m%d').dt.date  # remove HH:MM:SS
df.set_index('Date', inplace=True)

# Find spot prices
spot = df['C_Spot']
df.drop('C_Spot', axis=1, inplace=True)

T = sm.add_constant(range(spot.shape[0]))

model = sm.OLS(np.log(spot), T)
res = model.fit()  # Note this can deal with NaN in top row

print('Average annualized spot return=', 252 * res.params[1])
# Average annualized spot return= 0.02805562210100287

# Fitting gamma to forward curve
gamma = np.full(df.shape[0], np.nan)
for t in range(df.shape[0]):
    idx = np.where(np.isfinite(df.iloc[t, :]))[0]
    idxDiff = np.roll(idx, -1) - idx
    all_ones = all(idxDiff[0:4] == 1)

    if (len(idx) >= 5) and all_ones:
        FT = df.iloc[t, idx[:5]]
        T = sm.add_constant(range(FT.shape[0]))
        model = sm.OLS(np.log(FT.values), T)
        res = model.fit()
        gamma[t] = -12 * res.params[1]

pd.DataFrame(gamma).plot()

print('Average annualized roll return=', np.nanmean(gamma))
# Average annualized roll return= -0.12775650227459556


import math
from .utils import get_activities_over_time_as_list
from .state import State
import numpy as np

def shannon_entropy(string):
    """
    Calculates the Shannon entropy for the given string.

    :param string: any string, such as '000101001', '12402', or 'aBcd1234ef5g'

    :return: a real number representing the Shannon entropy
    """
    symbols = dict.fromkeys(list(string))
    symbol_probabilities = [float(string.count(symbol)) / len(string) for symbol in symbols]
    H = -sum([p_symbol * math.log(p_symbol, 2.0) for p_symbol in symbol_probabilities])
    return H + 0  # add 0 as a workaround so we don't end up with -0.0

def average_node_entropy(trajectory_or_activities):
    """
    Calculates the average node entropy in the given automaton evolution, where entropy is the Shannon entropy.
    The state of a node over time is represented as a string, and its entropy is calculated. The same is done for all
    nodes in this Network Automaton, and the average entropy is returned.

    :param trajectory_or_activities: a list of States that represents the evolution of the automaton, or a list of
                                     lists that represents the evolution of the automaton, where an inner list
                                     represents the activities of the nodes at a given timestep

    :return: a real number representing the average node Shannon entropy
    """
    if len(trajectory_or_activities) is 0:
        raise Exception("there are no activities")
    if isinstance(trajectory_or_activities[0], State):
        # trajectory_or_activities is a trajectory, convert it to an activities list
        activities = get_activities_over_time_as_list(trajectory_or_activities)
    else:
        activities = trajectory_or_activities
    num_cols = len(activities[0])
    entropies = []
    for i in range(0, num_cols):
        node_states_over_time = ''.join([str(x) for x in [y[i] for y in activities]])
        entropy = shannon_entropy(node_states_over_time)
        entropies.append(entropy)
    return np.mean(entropies)

def joint_shannon_entropy(stringX, stringY):
    """
    Calculates the joint Shannon entropy between the given strings, which must be of the same length.

    :param stringX: any string, such as '000101001', '12402', or 'aBcd1234ef5g'

    :param stringY: any string, such as '000101001', '12402', or 'aBcd1234ef5g'

    :return: a real number representing the joint Shannon entropy between the given strings
    """
    X = np.array(list(stringX))
    Y = np.array(list(stringY))
    joint_symbol_probabilities = []
    for x in set(X):
        for y in set(Y):
            joint_symbol_probabilities.append(np.mean(np.logical_and(X == x, Y == y)))
    return sum(-p * np.log2(p) for p in joint_symbol_probabilities if p != 0)

def mutual_information(stringX, stringY):
    """
    Calculates the mutual information between the given strings, which must be of the same length.

    :param stringX: any string, such as '000101001', '12402', or 'aBcd1234ef5g'

    :param stringY: any string, such as '000101001', '12402', or 'aBcd1234ef5g'

    :return: a real number representing the mutual information between the given strings
    """
    return shannon_entropy(stringX) + shannon_entropy(stringY) - joint_shannon_entropy(stringX, stringY)

def average_mutual_information(trajectory_or_activities, temporal_distance=1):
    """
    Calculates the average mutual information between a node and itself at the next n time steps, given by the
    specified temporal distance. A temporal distance of 1 means the next time step.
    For example, consider the following string, '00101010110', which represents the state of a node over 11 time steps.
    The strings which will be used for the computation of the mutual information between a node and itself at the
    next time step are: '0010101011' and '0101010110', since we pair each time-step value with its next value:
    " 00101010110"
    "00101010110 "

    :param trajectory_or_activities: a list of States that represents the evolution of the automaton, or a list of
                                     lists that represents the evolution of the automaton, where an inner list
                                     represents the activities of the nodes at a given timestep

    :param temporal_distance: the size of temporal separation, where the value must be greater than 0 and
                              less than the number of time steps.

    :return: a real number representing the average mutual information between a node and itself at the next time step
    """
    if len(trajectory_or_activities) is 0:
        raise Exception("there are no activities")
    if isinstance(trajectory_or_activities[0], State):
        # trajectory_or_activities is a trajectory, convert it to an activities list
        activities = get_activities_over_time_as_list(trajectory_or_activities)
    else:
        activities = trajectory_or_activities
    num_cols = len(activities[0])
    if not (0 < temporal_distance < num_cols):
        raise Exception("the temporal distance must be greater than 0 and less than the number of time steps")
    mutual_informations = []
    for i in range(0, num_cols):
        node_states_over_time = ''.join([str(x) for x in [y[i] for y in activities]])
        mi = mutual_information(node_states_over_time[:-temporal_distance], node_states_over_time[temporal_distance:])
        mutual_informations.append(mi)
    return np.mean(mutual_informations)


import numpy as np

# how MSE works:
# Takes each predictions deviance from the actual value, squares it, then
# averages the squared values. The Gauss-Markov theorem guarantees that the
# solution to linear regression is the best in the sense that the
# coefficients have the smallest expected squared error.
def MSE(target, predictions):
    squared_deviation = np.power(target - predictions, 2)
    return np.mean(squared_deviation)

# How MAD works:
# MAD is similar to MSE in that it averages to get the solution. The only
# differnce is that MAD is the absolute value of the difference between
# the prediction and the actual value.
def MAD(target, predictions):
    absolute_deviation = np.abs(target - predictions)
    return np.mean(absolute_deviation)


# https://www.hackerrank.com/challenges/np-transpose-and-flatten/problem

import numpy

len_row, len_column = [int(s) for s in input().split()]
# 4 3

arr_l = []
for _ in range(len_row):
    arr_l.append([int(s) for s in input().split()])
# 1 2 3 4
# 5 6 7 8
# 9 10 11 12
# 13 14 15 16

arr = numpy.array(arr_l)
# print(arr)
# [[ 1  2  3  4]
#  [ 5  6  7  8]
#  [ 9 10 11 12]
#  [13 14 15 16]]

print(arr.transpose())
# [[ 1  5  9 13]
#  [ 2  6 10 14]
#  [ 3  7 11 15]
#  [ 4  8 12 16]]

print(arr.flatten())
# [ 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16]

# print(arr)
# [[ 1  2  3  4]
#  [ 5  6  7  8]
#  [ 9 10 11 12]
#  [13 14 15 16]]

__Training__/Python - HackerRank/16. Numpy/Transpose and Flatten.py


import numpy as np

def log_likelihood_joint(points, pi, mu, sigma, **kwargs):
        """
        Args:
            points: NxD numpy array, the observations
            pi: np array of length K, the prior of each component
            mu: KxD numpy array, the center for each gaussian.
            sigma: KxD numpy array, the diagonal standard deviation of each gaussian.
        Return:
            ll(log-likelihood): NxK array, where ll(i, j) = log pi(j) + log NormalPDF(points_i | mu[j], sigma[j])

        Hint: Assume that the three dimensions of our multivariate gaussian are independent.
              This allows you to write treat it as a product of univariate gaussians.
        """
        lls = np.zeros(shape = (points.shape[0],np.size(pi)))
        for j in range(np.size(pi)):
            lls[:,j] = np.log(pi[j] + 1e-12)
            temp = points - mu[j,:]
            #cov = sigma[j,:]**(-1)
            x = 1
            for i in range(points.shape[1]):
                x *= 1/(np.sqrt(2 * np.pi) * np.sqrt(sigma[j, i]))
                y = -1/(2*(sigma[j, i]))
                x *= np.exp(y * (temp[:,i]**2))
            #(x-mu).transpose*inverse(covariance matrix)*(x-mu)
            #1xD*D*K
            #x = np.exp(-1/2 * np.sum(np.dot(temp, np.diag(cov)) * temp, axis = 1))

            lls[:,j] += np.log(x + 1e-12)
        return lls


import numpy as np

class GreedyKCenter(object):
    def fit(self, points, k):
        centers = []
        centers_index = []
        # Initialize distances
        distances = [np.inf for u in points]
        # Initialize cluster labels
        labels = [np.inf for u in points]

        for cluster in range(k):
            # Let u be the point of P such that d[u] is maximum
            u_index = distances.index(max(distances))
            u = points[u_index]
            # u is the next cluster center
            centers.append(u)
            centers_index.append(u_index)

            # Update distance to nearest center
            for i, v in enumerate(points):
                distance_to_u = self.distance(u, v)  # Calculate from v to u
                if distance_to_u < distances[i]:
                    distances[i] = distance_to_u
                    labels[i] = cluster

            # Update the bottleneck distance
            max_distance = max(distances)

        # Return centers, labels, max delta, labels
        self.centers = centers
        self.centers_index = centers_index
        self.max_distance = max_distance
        self.labels = labels

    @staticmethod
    def distance(u, v):
        displacement = u - v
        return np.sqrt(displacement.dot(displacement))


#!/usr/bin/python

from numpy import *

#	Input
#	X is the data matrix itself
#	rank is the expected number of eigen values
#	num_of_random_column is the number of random vectors to project down to

def random_svd(X, rank, num_of_random_column):

	if rank + num_of_random_column > X.shape[1]:
		num_of_random_column = X.shape[1] - rank

	random_matrix = random.normal(size=(X.shape[1], num_of_random_column))
	omega, r = linalg.qr(random_matrix, mode='reduced')
	X_hat = X.dot(X.T)

	Q, R = linalg.qr(X_hat.dot(X_hat).dot(X).dot(omega), mode='reduced')
	smaller_matrix = Q.T.dot(X)
	U,S,V = linalg.svd(smaller_matrix)
	U = Q.dot(U)

	return U,S,V

if __name__ == '__main__':
	#	print setting
	set_printoptions(suppress=True)
	set_printoptions(precision=3)

	#	program settings
	desired_rank = 5
	num_of_random_column = 10
	example_size = 20

	#	-------------------------
	X = random.normal(size=(example_size, example_size))
	U,Se,V1 = random_svd(X, desired_rank, num_of_random_column)
	U1 = U[0:10, 0:desired_rank]

	U,S,V2 = linalg.svd(X)
	U2 = U[0:10, 0:desired_rank]

	print 'Estimated Eigen Values : \n' , Se[0:desired_rank]
	print 'Real Eigen Values : \n' , S[0:desired_rank] , '\n'
	print 'Estimated U : '
	print U1 , '\n'
	print 'Real U : '
	print U2 , '\n'
	print 'Estimated V : '
	print V1 , '\n'
	print 'Real V : '
	print V2 , '\n'


"""
Five number summary example
"""
import numpy as np
# Students who has scored in Tamil Language
tamilMarks = np.array([82,74,56,35,89,78,90,100,84,65,54,59,45])
summaryTitles = ["Min","Lower Quartile", "Median", "Upper Quartile", "Maximum"]
tukey = [np.min(tamilMarks),
          np.percentile(tamilMarks, 25, interpolation='midpoint'),
          np.median(tamilMarks),
          np.percentile(tamilMarks, 75, interpolation='midpoint'),
          np.max(tamilMarks)]
for index, v in enumerate(tukey):
    print(summaryTitles[index], ':',v)


import numpy as np

def as_array(data):
    if np.isscalar(data):
        return np.array(data)

class Variable:
    def __init__(self, data):
        if not isinstance(data, np.ndarray):
            raise TypeError(f"{type(data)} type is not supported")
        self.data = data
        self.grad = None
        self.creator = None

    def set_creator(self, creator_func):
        self.creator = creator_func

    def backward(self):
        if self.grad is None:
            self.grad = np.ones_like(self.data)
        funcs = [self.creator]
        while funcs:
            f = funcs.pop()
            x, y = f.input, f.output
            x.grad = f.backward(y.grad)
            if x.creator is not None:
                funcs.append(x.creator)

class Function:
    def __call__(self, x: Variable):
        y = self.forward(x.data)
        output = Variable(as_array(y))
        output.set_creator(self)
        self.input = x
        self.output = output
        return output

    def forward(self, x_data):
        raise NotImplementedError()

    def backward(self, dout):
        raise NotImplementedError()

class Square(Function):
    def forward(self, x_data):
        return x_data ** 2

    def backward(self, dout):
        x_data = self.input.data
        return dout * 2 * x_data

class Exp(Function):
    def forward(self, x_data):
        return np.exp(x_data)

    def backward(self, dout):
        x_data = self.input.data
        return dout * np.exp(x_data)

def square(x):
    return Square()(x)

def exp(x):
    return Exp()(x)

if __name__ == "__main__":
    x = Variable(np.array(0.5))
    y = square(exp(square(x)))
    y.backward()
    print(f"x.grad is : {x.grad}")


# coding=utf-8
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

# 构造训练数据
x = np.arange(0., 10., 0.2)
m = len(x)  # 训练数据点数目
x0 = np.full(m, 1.0)
input_data = np.vstack([x0, x]).T  # 将偏置b作为权向量的第一个分量
target_data = 2 * x + 5 + np.random.randn(m)

# 两种终止条件
loop_max = 10000  # 最大迭代次数(防止死循环)
epsilon = 1e-3

# 初始化权值
np.random.seed(0)
w = np.random.randn(2)
# w = np.zeros(2)

alpha = 0.001  # 步长(注意取值过大会导致振荡,过小收敛速度变慢)
diff = 0.
error = np.zeros(2)
count = 0  # 循环次数
finish = 0  # 终止标志
error_list = []

# -------------------------------------------随机梯度下降算法----------------------------------------------------------
while count < loop_max:
    count += 1

    # 遍历训练数据集，不断更新权值
    for i in range(m):
        diff = np.dot(w, input_data[i]) - target_data[i]  # 训练集代入,计算误差值

        # 采用随机梯度下降算法,更新一次权值只使用一组训练数据
        w = w - alpha * diff * input_data[i]
        sum_m = np.sum(diff * input_data[i])
        error_list.append(sum_m**2)
        # ------------------------------终止条件判断-----------------------------------------
        # 若没终止，则继续读取样本进行处理，如果所有样本都读取完毕了,则循环重新从头开始读取样本进行处理。

    # ----------------------------------终止条件判断-----------------------------------------
    # 注意：有多种迭代终止条件，和判断语句的位置。终止判断可以放在权值向量更新一次后,也可以放在更新m次后。
    if np.linalg.norm(w - error) < epsilon:     # 终止条件：前后两次计算出的权向量的绝对误差充分小
        finish = 1
        break
    else:
        error = w
print 'loop count = %d' % count,  '\tw:[%f, %f]' % (w[0], w[1])

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

# check with scipy linear regression
slope, intercept, r_value, p_value, slope_std_error = stats.linregress(x, target_data)
print 'intercept = %s slope = %s' % (intercept, slope)

plt.plot(range(len(error_list[0:100])), error_list[0:100])
plt.show()

plt.plot(x, target_data, 'k+')
plt.plot(x, w[1] * x + w[0], 'r')
plt.show()


"""
Basado en el ejemplo de clase
Solución LAB1
"""

import os
import numpy as np
from matplotlib import pyplot as plt
from scipy.io.wavfile import write as write_wav
from scipy.fft import fft, fftfreq
from scipy.fft import rfft, rfftfreq, irfft

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
audio_filename = os.path.join(BASE_DIR, 'SUCIA.wav')

FS = 44100  # Frecuencia de muestreo
DURATION = 3  # segundos

TONE = 440                 # Tono
NOISE = 3200               # Ruido
muestras_grafica = 1000    # Cantidad de muestras a graficar

def generate_sine_wave(freq, sample_rate=FS, duration=DURATION):
    """Genera una onda senoidal con frecuencia freq
    con una cantidad de muestras = sample_rate * duration"""
    x = np.linspace(0, duration, sample_rate * duration, endpoint=False)
    frequencies = x * freq
    # Sin(wt), w = 2pi*f
    y = np.sin((2 * np.pi) * frequencies)
    return x, y

# Generando tono deaseado
x, y = generate_sine_wave(TONE)
x_limited = x[:muestras_grafica]
y_limited = y[:muestras_grafica]
plt.plot(x_limited, y_limited)
plt.xlabel('Tiempo')
plt.ylabel('Amplitud')
plt.title('Señal pura con frecuencia f')
plt.show()
# Tono Deseado
tone = y

# Generando ruido
_, noise1 = generate_sine_wave(NOISE, FS, DURATION)
mixed_tone = tone + noise1
normalized =  np.int16((mixed_tone / mixed_tone.max()) * 32767)
print('Generando audio con ruido')
write_wav(audio_filename, FS, normalized)
plt.plot(normalized[:muestras_grafica])
plt.xlabel('Tiempo')
plt.ylabel('Amplitud')
plt.title('Señal Tono + Ruido')
plt.show()

# FFT
# Note the extra 'r' at the front
N = FS * DURATION
yf = rfft(normalized)
xf = rfftfreq(N, 1 / FS)
plt.plot(xf, np.abs(yf))
plt.xlabel('Frecuencia')
plt.title('Cuadrante positivo')
plt.show()

# Filtrando la señal
# The maximum frequency is half the sample rate
points_per_freq = len(xf) / (FS / 2)
print(f'points_per_freq {points_per_freq}')
# Our target frequency is noise freq in  Hz
target_idx = int(points_per_freq * NOISE)
yf[target_idx - 1:target_idx + 2] = 0
plt.plot(xf, np.abs(yf))
plt.xlabel('Frecuencia')
plt.title('Filtrado Ruido')
plt.show()

# FFT Inversa
new_sig = irfft(yf)
plt.plot(new_sig[:muestras_grafica])
plt.xlabel('Tiempo')
plt.ylabel('Amplitud')
plt.title('Señal filtrada')
plt.show()

# Generando audio filtrado (limpio)
norm_new_sig = np.int16(new_sig * (32767 / new_sig.max()))
write_wav(
    os.path.join(BASE_DIR, 'LIMPIA.wav'),
    FS, norm_new_sig
)


import numpy as np

class Optimize:

    """
    Instantiate a function.

    :param func: The lambda function to evaluate.
    :type func: Lambda function object

    :param dx: The derivative of the function passed to 'func'. Optional - Used only in Newton-Raphson method.
    :type dx: Lambda function object
    """
    def __init__(self, func, dx = None, ddx = None):
        self.func = func
        self.dx = dx
        self.ddx = ddx

    def golden_search(self, lbound: float, ubound: float, max_itn = 100, delta = 0.000001):
        """
        Finding the root of a function using bisection method.

        :param lbound: The lower bound of the range to evaluate.
        :type lbound: int

        :param ubound: The upper bound of the range to evaluate.
        :type ubound: int

        :param max_itn: The maximum number of iterations to evaluate.
        :type itn: int

        :param delta: The error threshhold.
        :type itn: float

        :return: Numpy array of iteration steps.
        :rtype: ndarray
        """

        phi = (1 + 5**0.5)/2
        a = lbound
        b = ubound
        c = b + (a - b)/phi
        d = a + (b - a)/phi
        fc = self.func(c)
        fd = self.func(d)
        itn = 0
        arr = [itn, a, c, d, b]

        while abs(b - a) >= delta and itn <= max_itn:
            if fc < fd:
                b = d
                d = c
                fd = fc
                c = b + (a - b)/phi
                fc = self.func(c)
            else:
                a = c
                c = d
                fc = fd
                d = (b -  a)/phi
                fd = self.func(d)

            itn += 1
            arr = np.vstack([arr, [itn, a, c, d, b]])

        return arr

    def fibonacci(self, lbound: float, ubound: float, max_itn = 100, delta = 0.0001):
        """
        Finding the root of a function using bisection method.

        :param lbound: The lower bound of the range to evaluate.
        :type lbound: int

        :param ubound: The upper bound of the range to evaluate.
        :type ubound: int

        :param max_itn: The maximum number of iterations to evaluate.
        :type itn: int

        :param delta: The error threshhold.
        :type itn: float

        :return: Numpy array of iteration steps.
        :rtype: ndarray
        """

        a = lbound
        b = ubound
        farr = [1, 1]
        n = 2

        while True:
            farr.append(farr[n-1] + farr[n-2])

            if farr[n] > (b - a)/delta:
                break

            n += 1

        c = a + farr[n-2]/farr[n]*(b-a)
        d = a + farr[n-1]/farr[n]*(b-a)

        fc = self.func(c)
        fd = self.func(d)

        itn = 0

        arr = [itn, a, b, c, d]

        n = n - 1

        while itn <= max_itn and n >= 2: #abs(b - a) >= delta and
            if fc < fd:
                b = d
                d = c
                fd = fc
                c = a + farr[n-2]/farr[n]*(b-a)
                fc = self.func(c)
            else:
                a = c
                c = d
                fc = fd
                d = a + farr[n-1]/farr[n]*(b-a)
                fd = self.func(d)

            itn += 1
            arr = np.vstack([arr, [itn, a, c, d, b]])

            n = n - 1

        return arr

        return

    def newton(self, x0: float, max_itn = 100, delta = 1e-6):
        """
        Finding the root of a function using Newton-Raphson method.

        :param x0: The initial value to evaluate.
        :type x0: int

        :param max_itn: The maximum number of iterations to evaluate.
        :type itn: int

        :param delta: The error threshhold.
        :type itn: float

        :return: Numpy array of iteration steps.
        :rtype: ndarray
        """

        x = x0
        itn = 0

        while itn <= max_itn and abs(self.dx(x)/self.ddx(x)) >= delta:
            x = x - (self.dx(x)/self.ddx(x))

            if itn == 0:
                arr = [itn, x, self.dx(x), self.ddx(x), abs(self.dx(x)/self.ddx(x))]

            else:
                arr = np.vstack([arr, [itn, x, self.dx(x), self.ddx(x), abs(self.dx(x)/self.ddx(x))]])
            itn += 1

        return arr

    def secant(self, x0: float, x1: float, max_itn = 100, delta = 1e-6):
        """
        Finding the root of a function using secant method.

        :param x0: The lower bound of the range to evaluate.
        :type lbound: int

        :param x1: The upper bound of the range to evaluate.
        :type ubound: int

        :param max_itn: The maximum number of iterations to evaluate.
        :type itn: int

        :param delta: The error threshhold.
        :type itn: float

        :return: Numpy array of iteration steps.
        :rtype: ndarray
        """

        a = x0
        b = x1
        lst = [a, b]
        i = 2
        itn = 0

        if self.dx(a)*self.dx(b) > 0:
            return 'Invalid upper and lower limits'

        else:
            while itn <= max_itn and abs(a - b) >= delta:
                lst.append(lst[i-1] - (self.dx(lst[i - 1])*(lst[i-1] - lst[i-2]))/(self.dx(lst[i-1]) - self.dx(lst[i-2])))
                if itn == 0:
                    arr = [itn, i, lst[i-1], lst[i], self.dx(lst[i])]
                else:
                    arr = np.vstack([arr, [itn, i, lst[i-1], lst[i], self.dx(lst[i])]])
                a = lst[i - 2]
                b = lst[i - 1]
                itn += 1
                i += 1
            return arr
        return

    def line_search(self):
        return

    def hooke_jeeves(self):
        return

    def bisection(self, lbound: float, ubound: float, max_itn = 100, delta = 1e-6):
        """
        Finding the root of a function using bisection method.

        :param lbound: The lower bound of the range to evaluate.
        :type lbound: int

        :param ubound: The upper bound of the range to evaluate.
        :type ubound: int

        :param max_itn: The maximum number of iterations to evaluate.
        :type itn: int

        :param delta: The error threshhold.
        :type itn: float

        :return: Numpy array of iteration steps.
        :rtype: ndarray
        """

        a = lbound
        b = ubound
        itn = 0

        if self.dx(a)*self.dx(b) > 0:
            return 'Invalid upper and lower limits'

        else:
            while itn <= max_itn and abs(a-b) >= delta:
                x = (a + b)/2
                if itn == 0:
                    arr = [a, b, x, self.dx(x)]
                else:
                    arr = np.vstack([arr, [a, b, x, self.dx(x)]])
                if self.dx(x) > 0:
                    b = x
                elif self.dx(x) < 0:
                    a = x
                else:
                    return arr
                itn += 1

            return arr

    def steepest_decent():
        return


import numpy as np
from rebox import BBoxFormat
from rebox import BBox

def iou(a:BBox, b:BBox):
    """
    Compute the Jaccarrd Index / Intersection over Union of a pair of 2D bounding boxes.

    Args:
        a: BBox object
        b: BBox object

    Returns:
        The IoU of the two bounding boxes.
    """
    # dealing with various formats:
    # unify style to XYXY,
    # keep scale, and account for it in algorithm

    scale = a.format.scale

    unified_format = BBoxFormat("XYXY", scale)

    a = a.as_format(unified_format)
    b = b.as_format(unified_format)

    xA = np.maximum(a.x1, b.x1)
    yA = np.maximum(a.y1, b.y1)
    xB = np.maximum(a.x2, b.x2)
    yB = np.maximum(a.y2, b.y2)

    if scale is None:
        inter_w = xB - xA + 1
    else:
        inter_w = xB - xA

    inter_w = inter_w * (inter_w >= 0)

    if scale is None:
        inter_h = yB - yA + 1
    else:
        inter_h = yB - yA

    inter_h = inter_h * (inter_h >= 0)

    intersection = inter_w * inter_h

    a_width = a.x2 - a.x1
    a_height = a.y2 - a.y1
    a_area = a_width * a_height

    b_width = b.x2 - b.x1
    b_height = b.y2 - b.y1
    b_area = b_width * b_height

    iou = intersection / (a_area + b_area - intersection)

    if np.isinf(iou) or np.isnan(iou):
        iou = 0

    return iou


import numpy as np
import math

def simulate_rss_matrix(num_nodes, area_side, params=None, threshold=None):
    """This function generates a random set of device locations and
      their corresponding RSS matrix

    Parameters:
        num_nodes (int): number of nodes to simulate
        area_side (float): length of one side of the square area, in meters
        params (4-tuple float): (d_ref, power_ref, path_loss_exp, stdev_power)
            d_ref is the reference distance in m
            power_ref is the received power at the reference distance
            path_loss_exp is the path loss exponent
            stdev_power is standard deviation of received Power in dB
        threshold (float): RSS value in dBm below which the radio cannot receive
    Returns:
        (node_locs, rss_matrix): both numpy arrays of float values, where
            node_locs: (num_nodes, 2)-sized array giving x and y
                       coordinates of devices in meters
            rss_matrix: (num_nodes, num_nodes)-sized array giving RSS values
                       between pairs of devices in dB. rss_matrix[i,j] gives
                       RSS at device j when device i transmits. For any entry
                       that is too small, as well as for the diagonal entries,
                       it returns threshold.
    """

    if params is None:
        params = (1.0, -46.123, 2.902, 3.940)
    if threshold is None:
        threshold = -95
    # the above values are arbitrarily chosen "default values"
    # should be changed based on measurements

    node_locs = area_side*np.random.random_sample((num_nodes, 2))

    d_ref = params[0] # reference distance
    power_ref = params[1] # mean received power at reference distance
    path_loss_exp = params[2] # path loss exponent
    stdev_power = params[3] # standard deviation of received power

    #initialize rss_matrix with the radio rss threshold values as default
    rss_matrix = np.full((num_nodes, num_nodes), float(round(threshold,3)))

    for i in range(num_nodes):
        for j in range(num_nodes):
            if i !=j :
                dist = math.sqrt( (node_locs[i,0] - node_locs[j,0])**2 + (node_locs[i,1] - node_locs[j,1])**2)
                rss_matrix[i,j] = power_ref - path_loss_exp*10*math.log10(dist/d_ref) + float(np.random.normal(0, stdev_power, 1)) #lognormal model
                rss_matrix[i,j] = max(rss_matrix[i,j], threshold) #caps minimum
                rss_matrix[i,j] = round(rss_matrix[i][j],2)

    return (node_locs, rss_matrix)

## uncomment the following to test the function out
# if __name__ == '__main__':
#     nl, rm = simulate_rss_matrix(4, 100)
#     print("Node locations array")
#     print(nl)
#     print("RSS Matrix")
#     print(rm)


#########################
##                     ##
## Irving Gomez Mendez ##
##     May 05, 2021    ##
##                     ##
#########################
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import statsmodels.api as sm
from scipy.stats import norm, poisson

# This example considers the number of cases of AIDS in Belgium (data from the 80's).
# We assume that a Poisson model is razonable (See book of Wood S.S. (2006) Generalized Additive Models).
cases = [12,14,33,50,67,74,123,141,165,204,253,246,240]
x = np.array(range(13))
years = x+1981

# significance level
alpha = 0.05

plt.figure(figsize=(10,7.5))
plt.plot(years, cases, 'o')
plt.xlabel('year')
plt.ylabel('cases')
plt.title("Cases of AIDS in Belgium")

n = len(cases)
X = np.vstack([np.ones(n), x]).T

# Poisson regression by hand
b = [3,0.2] # initial values

tolm   = 1e-6       # tolerance (minimum norm of the difference of the betas)
iterm  = 100        # maximum number of iterations
tolera = 1          # initialize tolera
itera  = 0          # initialize ittera
histo  = b          # initialize beta upgrade

while((tolera > tolm) and (itera < iterm)):
  lam    = np.exp(X @ b)
  D      = np.diag(lam)
  var_b  = np.linalg.inv(X.T @ D @ X)
  delta  = var_b @ X.T @ (cases-lam)
  b      = b+delta
  tolera = np.sqrt(sum(delta**2))
  histo  = np.vstack([histo, b])
  itera  = itera+1

histo

# Let's get the significance of the estimators
se_b = np.sqrt(np.diag(var_b))
z_score = b/se_b
p_value = 1-norm.cdf(np.abs(z_score))

z_score
p_value

plt.figure(figsize=(10,7.5))
plt.plot(years, cases, 'o', label='data')
plt.plot(years, lam, 'o', label='predicted')
plt.xlabel('year')
plt.ylabel('cases')
plt.title("Cases of AIDS in Belgium")
plt.legend(loc='best')

# Let's plot using the log(cases)
plt.figure(figsize=(10,7.5))
plt.plot(years, np.log(cases), 'o', label='data')
plt.plot(years, X @ b, 'o', label='estimated')
plt.xlabel('year')
plt.ylabel('cases')
plt.title("log of cases of AIDS in Belgium")
plt.legend(loc='best')

# It looks like there is a lack of structre, lets consider a quadratic fit
X = np.vstack([np.ones(n), x, x**2]).T

# Poisson regression by hand
b = [3,0.2,0.2] # initial values

tolm   = 1e-6       # tolerance (minimum norm of the difference of the betas)
iterm  = 100        # maximum number of iterations
tolera = 1          # initialize tolera
itera  = 0          # initialize ittera
histo  = b          # initialize beta upgrade

while((tolera > tolm) and (itera < iterm)):
  lam    = np.exp(X @ b)
  D      = np.diag(lam)
  var_b  = np.linalg.inv(X.T @ D @ X)
  delta  = var_b @ X.T @ (cases-lam)
  b      = b+delta
  tolera = np.sqrt(sum(delta**2))
  histo  = np.vstack([histo, b])
  itera  = itera+1

# Let's get the significance of the estimators
se_b = np.sqrt(np.diag(var_b))
z_score = b/se_b
p_value = 1-norm.cdf(np.abs(z_score))

p_value

plt.figure(figsize=(10,7.5))
plt.plot(years, cases, 'o', label='data')
plt.plot(years, lam, 'o', label='predicted')
plt.xlabel('year')
plt.ylabel('cases')
plt.title("Cases of AIDS in Belgium")
plt.legend(loc='best')

# Let's plot using the log(cases)
plt.figure(figsize=(10,7.5))
plt.plot(years, np.log(cases), 'o', label='data')
plt.plot(years, X @ b, 'o', label='predicted')
plt.xlabel('year')
plt.ylabel('cases')
plt.title("log of cases of AIDS in Belgium")
plt.legend(loc='upper left')

# Let's add confidence and prediction intervals
upp_conf = X @ b + norm.ppf(1-alpha/2)*np.sqrt(np.diag(X @ var_b @ X.T))
low_conf = X @ b - norm.ppf(1-alpha/2)*np.sqrt(np.diag(X @ var_b @ X.T))

# Let's plot using the log(cases)
plt.figure(figsize=(10,7.5))
plt.fill_between(years, low_conf, upp_conf, facecolor='yellow', alpha=0.5, label=' Approx. confidence interval')
plt.plot(years, np.log(cases), 'o', label='data')
plt.plot(years, X @ b, 'o', label='estimated')
plt.xlabel('year')
plt.ylabel('cases')
plt.title("log of cases of AIDS in Belgium")
plt.legend(loc='upper left')

upp_pred = poisson.ppf(1-alpha/2, lam)
low_pred = poisson.ppf(alpha/2, lam)

plt.figure(figsize=(10,7.5))
plt.fill_between(years, low_pred, upp_pred, facecolor='green', alpha=0.5, label='Prediction interval')
plt.plot(years, cases, 'o', label='data')
plt.plot(years, lam, 'o', label='predicted')
plt.xlabel('year')
plt.ylabel('cases')
plt.title("Cases of AIDS in Belgium")
plt.legend(loc='upper left')

upp_conf_y_scale = lam + norm.ppf(1-alpha/2)*np.sqrt(np.exp(2*X @ b) * np.diag(X @ var_b @ X.T))
low_conf_y_scale = lam - norm.ppf(1-alpha/2)*np.sqrt(np.exp(2*X @ b) * np.diag(X @ var_b @ X.T))

# We can add the confidence interval, with the warning due to Jensen's inequality
plt.figure(figsize=(10,7.5))
plt.fill_between(years, low_pred, upp_pred, facecolor='green', alpha=0.5, label='Prediction interval')
plt.fill_between(years, low_conf_y_scale, upp_conf_y_scale, facecolor='yellow', alpha=0.5, label='Approx. confidence interval')
plt.plot(years, cases, 'o', label='data')
plt.plot(years, lam, 'o', label='predicted')
plt.xlabel('year')
plt.ylabel('cases')
plt.title("Cases of AIDS in Belgium")
plt.legend(loc='upper left')

# Using sm.GLM
poisson_model = sm.GLM(cases, X, family=sm.families.Poisson())
poisson_results = poisson_model.fit()
poisson_results.summary()

b
se_b
z_score
p_value

###

import numpy,from scipy,import statsmodels