# Question 06, Lab 04
# AB Satyaprakash - 180123062

# imports ----------------------------------------------------------------------------
from sympy.abc import t
from sympy import evalf, integrate, sqrt, sin
from math import pi
import numpy as np
import pandas as pd

# functions --------------------------------------------------------------------------

def f(func, x):
    return func.subs(t, x)

def getX(a, b, n):
    h = (b-a)/n
    return np.arange(a, b+h/2, h)

def compositeTrapezoidalRule(func, X, mapf, count):
    sum = 0
    a, b = X[0], X[-1]
    n = X.shape[0]
    h = (b-a)/(n-1)
    cnt = count
    for i in range(X.shape[0]):
        x = X[i]
        if(mapf.get(x) == None):
            mapf[x] = f(func, x)
            val = mapf[x]
            cnt += 1
        else:
            val = mapf[x]
        if(i == 0 or i == n-1):
            sum += val/2
        else:
            sum += val
    return (h*sum), cnt

# program body
# part (a)
func = t/(1+t**2)
a, b = 0, 3
mapf = {}
n = 1
count = 0

H_2, TH_2, errorArray = [], [], []
while(True):
    h = (b-a)/n
    X = getX(a, b, n)
    Th, count = compositeTrapezoidalRule(func, X, mapf, count)
    X = getX(a, b, 2*n)
    Th_2, count = compositeTrapezoidalRule(func, X, mapf, count)
    err = abs(Th-Th_2)/abs(Th_2)
    # append values to tabulate later on
    H_2.append(h/2)
    TH_2.append(Th_2)
    errorArray.append(err)
    # error break condition
    if(err < 10**(-6)):
        break
    n *= 2

print('For part a:')
H_2, TH_2, errorArray = np.array(H_2), np.array(TH_2), np.array(errorArray)
data = np.vstack((H_2, TH_2, errorArray)).T
df = pd.DataFrame(data,
                  columns=['h/2', 'T(h/2)', '|T(h)-T(h/2)|/|T(h/2)|'])
print('\n', df.round(6), '\n')
print('Estimated value of integral is', TH_2[-1])
print('The total number of function evaluations f(x) is', count)
print('\n')

# part (b)
func = 1/(1-t)
a, b = 0, 0.95
mapf = {}
n = 1
count = 0
H_2, TH_2, errorArray = [], [], []
while(True):
    h = (b-a)/n
    X = getX(a, b, n)
    Th, count = compositeTrapezoidalRule(func, X, mapf, count)
    X = getX(a, b, 2*n)
    Th_2, count = compositeTrapezoidalRule(func, X, mapf, count)
    err = abs(Th-Th_2)/abs(Th_2)
    # append values to tabulate later on
    H_2.append(h/2)
    TH_2.append(Th_2)
    errorArray.append(err)
    # error break condition
    if(err < 10**(-6)):
        break
    n *= 2

print('For part b:')
H_2, TH_2, errorArray = np.array(H_2), np.array(TH_2), np.array(errorArray)
data = np.vstack((H_2, TH_2, errorArray)).T
df = pd.DataFrame(data,
                  columns=['h/2', 'T(h/2)', '|T(h)-T(h/2)|/|T(h/2)|'])
print('\n', df.round(6), '\n')
print('Estimated value of integral is', TH_2[-1])
print('The total number of function evaluations f(x) is', count)
print('\n')

# part (c)
M = [0.5, 0.8, 0.95]
for i in range(3):
    m = M[i]
    func = 1/sqrt(1-m*(sin(t)**2))
    a, b = 0, pi/2
    mapf = {}
    n = 1
    count = 0
    H_2, TH_2, errorArray = [], [], []
    while(True):
        h = (b-a)/n
        X = getX(a, b, n)
        Th, count = compositeTrapezoidalRule(func, X, mapf, count)
        X = getX(a, b, 2*n)
        Th_2, count = compositeTrapezoidalRule(func, X, mapf, count)
        err = abs(Th-Th_2)/abs(Th_2)
        # append values to tabulate later on
        H_2.append(h/2)
        TH_2.append(Th_2)
        errorArray.append(err)
        # error break condition
        if(err < 10**(-6)):
            break
        n *= 2

    print('For part c.{} with m = {}:'.format(i+1, m))
    H_2, TH_2, errorArray = np.array(H_2), np.array(TH_2), np.array(errorArray)
    data = np.vstack((H_2, TH_2, errorArray)).T
    df = pd.DataFrame(data,
                      columns=['h/2', 'T(h/2)', '|T(h)-T(h/2)|/|T(h/2)|'])
    print('\n', df, '\n')
    print('Estimated value of integral is', TH_2[-1])
    print('The total number of function evaluations f(x) is', count)
    print('\n')

Semester 6/MA 322 (Scientific Computing Theory)/Labs/Lab 4/Code/q6.py


import numpy as np
from copy import deepcopy
from scipy.stats import multivariate_normal

def sample_mixture(theta0, sigma0, pi0, n):
    d = theta0.shape[1]
    x = np.empty((n,d))

    for i in range(n):
        u = np.random.uniform(size=1)

        this_pi = 0
        for j in range(pi0.size):
            this_pi += pi0[j]

            if u < this_pi:
                x[i,:] = np.random.multivariate_normal(mean=theta0[j,:], cov=sigma0[j,:,:], size=1)
                break

    return x

def density(X, theta, sigma):
    out = np.empty((X.shape[0], theta.shape[0]))

    for i in range(theta.shape[0]):
        out[:,i] = multivariate_normal.pdf(X, mean=theta[i,:], cov=sigma[i,:,:])

    return out

def em(X, theta_start, sigma_start, pi_start, max_iter=5000, eps=1e-8, mix_type="weak", xi=3):

    theta_prev = theta_start
    sigma_prev = sigma_start
    pi_prev    = pi_start

    theta_new = theta_start
    sigma_new = sigma_start
    pi_new    = pi_start

    K = np.size(pi_start)
    n = X.shape[0]
    d = X.shape[1]

    T1 = [None] * K
    T2 = [None] * K
    T3 = [None] * K

    for iiter in range(max_iter):

        density_eval = np.array(density(X, theta_new, sigma_new))
        weights = np.tile(pi_new, (n, 1)) * density_eval#.reshape([-1,1])
        weights /= np.tile(np.sum(weights, 1), (K, 1)).T

        for i in range(K):
            T1[i] = np.sum(weights[:,i])
            T2[i] = np.sum(weights[:,i].reshape([-1,1]) * X, axis=0).reshape([1,d])

        pi_new = [(T1[i] + xi) / (n + K * xi) for i in range(K)]

        for i in range(K):
            theta_new[i,:] = T2[i]/T1[i]

        if mix_type == "weak":
            # Use speedup for variance proposed by McLachlan and Peel (2000).
            for i in range(K):
                T3 = np.dot(X.T, weights[:,i].reshape([-1,1]) * X)
                sigma_new[i,:,:] = (T3 - ((1.0/T1[i] ) * np.outer(T2[i], T2[i]))) / T1[i]

        if iiter > 500 or (iiter > 100 and iiter % 10 == 0):
           if np.linalg.norm(theta_new - theta_prev) + np.linalg.norm(sigma_new - sigma_prev) < eps or iiter > max_iter:
               break

        theta_prev = deepcopy(theta_new)
        sigma_prev = deepcopy(sigma_new)
        pi_prev    = deepcopy(pi_new)

    return (theta_new, sigma_new, pi_new, iiter)


import numpy as np
import time

print('1. 创建大小为 10 的空向量')
a = np.zeros(10)
print(a)

print('2. 查看矩阵占据的内存大小')
print('用元素个数乘以每个元素的大小')
print(f'占据 {a.size * a.itemsize} 字节')

print('3. 创建一个向量，值从 10 到 49')
a = np.arange(10, 50)
print(a)

print('4. 翻转一个向量')
a = a[::-1]
print(a)

print('5. 创建一个 3x3 的矩阵，值从 0 到 8')
a = np.arange(9).reshape(3,3)
print(a)

print('6. 从 [1, 2, 0, 0, 4, 0] 中寻找非零元素索引')
nz = np.nonzero([1, 2, 0, 0, 4, 0])
print(nz)

print('7. 创建 3x3 单位矩阵（对角线元素为 1 的方阵）')
a = np.eye(3)
print(a)

print('8. 创建一个 3x3x3 的随机矩阵')
a = np.random.random((3, 3, 3))
print(a)

print('9. 创建一个 10x10 的矩阵并寻找最大最小值')
a = np.random.random((10, 10))
a_min, a_max = a.min(), a.max()
print('min', a_min, ', max', a_max)

print('10. 创建一个长度为 30 的向量，并求均值')
a = np.random.random(30)
print('mean', a.mean())

print('11. 创建一个边界为 1 其他为 0 的二维矩阵')
a = np.ones((10, 10))
a[1:-1,1:-1] = 0
print(a)

print('12. 为已经存在的矩阵填充 0 的边界')
a = np.ones((5, 5))
print(a)
a = np.pad(a, pad_width=1, mode='constant', constant_values=0)
print(a)

print('13. 给出下列计算的结果')
print('0 * np.nan =', 0 * np.nan)
print('np.nan == np.nan =', np.nan == np.nan)
print('np.inf > np.nan =', np.inf > np.nan)
print('np.nan - np.nan =', np.nan - np.nan)
print('np.nan in set([np.nan]) =', np.nan in set([np.nan]))
print('0.3 == 3 * 0.1 =', 0.3 == 3 * 0.1)

print('14. 创建一个 5x5 的矩阵，对角线下的数值为 1 2 3 4')
a = np.diag(1 + np.arange(4), k=-1)
print(a)

print('15. 创建一个 8x8 矩阵，其中 0 和 1 间隔分布')
a = np.zeros((8, 8), dtype=int)
a[1::2, ::2] = 1
a[::2, 1::2] = 1
print(a)

print('16. 使用 tile 函数创建一个 8x8 矩阵，其中 0 和 1 间隔分布')
a = np.tile(np.array([[0, 1], [1, 0]]), (4, 4))
print(a)

print('17. 假设有一个 (6, 7, 8) 大小的矩阵，那么第 100 个元素的索引是多少')
print(np.unravel_index(100, (6, 7, 8)))

print('18. 归一化一个随机 5x5 矩阵')
a = np.random.random((5, 5))
a = (a - np.mean(a)) / np.std(a)
print(a)

print('19. 点乘一个 5x3 和 3x2 的矩阵')
a = np.dot(np.ones((5, 3)), np.ones((3, 2)))
print(a)

print('20. 给定一个一维数组，不新增空间，把 3~8 之间的数字变成负数')
a = np.arange(10)
a[(3 < a) & (a <= 8)] *= -1
print(a)

print('21. 两个数组求交集')
a1 = np.random.randint(0, 10, 10)
a2 = np.random.randint(0, 10, 10)
print(np.intersect1d(a1, a2))

print('22. 获取 2020 年 6 月的所有日期')
a = np.arange('2020-06', '2020-07', dtype='datetime64[D]')
print(a)

print('23. 用 5 种方法去掉小数部分')
a = np.random.uniform(0, 10, 10)
print('a', a)
print('1:', a - a%1)
print('2:', np.floor(a))
print('3:', np.ceil(a) - 1)
print('4:', a.astype(int))
print('5:', np.trunc(a))

print('24. 创建一个 5x5 的矩阵，每一行都是从 0 到 4')
a = np.zeros((5, 5))
a += np.arange(5)
print(a)

print('25. 创建一个大小为 10，值从 0 到 1 的向量(不包括 0 和 1)')
a = np.linspace(0, 1, 11, endpoint=False)[1:]
print(a)

print('26. 创建一个大小为 10 的随机向量并排序')
a = np.random.random(10)
a.sort()
print(a)

print('27. 如何用比 np.sum 更快的方法对一个小数组求和')
a = np.arange(10)
print('a', a)
start = time.time()
print('add.reduct', np.add.reduce(a))
end = time.time()
print('add.reduce time:', end-start)
start = time.time()
print('np.sum', np.sum(a))
end = time.time()
print('np.sum time:', end - start)

print('28. 比较两个数组是否相等')
a = np.random.randint(0, 10, 10)
b = np.random.randint(0, 10, 10)
print(np.allclose(a, b))
print(np.array_equal(a, b))

print('29. 将一个 10x2 的笛卡尔坐标系的点转成极坐标')
a = np.random.random((10, 2))
x, y = a[:, 0], a[:, 1]
r = np.sqrt(x**2 + y**2)
t = np.arctan2(y, x)
print(r)
print(t)

print('30. 创建一个大小为 10 的随机向量，并将最大的替换成 0')
a = np.random.random(10)
print('before', a)
a[a.argmax()] = 0
print('after', a)

print('31. 不用额外空间将 float 矩阵变成 int 矩阵')
a = np.arange(10, dtype=np.float32)
a = a.astype(np.int32, copy=False)
print(a)

print('32. 在一个 2 维矩阵中随机放 p 个元素')
n, p = 10, 3
a = np.zeros((n, n))
np.put(a, np.random.choice(range(n*n), p, replace=False), 1)
print(a)

print('33. 矩阵的每行减去每行的均值')
a = np.random.randint(0, 10, (5, 10))
print('before', a)
b = a - a.mean(axis=1, keepdims=True)
print('after', b)

print('34. 根据第 i 列给矩阵排序')
a = np.random.randint(0, 10, (3, 3))
print('before', a)
print('after', a[a[:, 1].argsort()])

print('35. 交换矩阵的两行')
a = np.arange(25).reshape(5, 5)
a[[0,1]] = a[[1, 0]]
print(a)

print('36. 如何计算一个数组的滑动窗口')
def moving_averate(a, n=3):
    ret = np.cumsum(a, dtype=float)
    ret[n:] = ret[n:] - ret[:-n]
    return ret[n-1:] / n
a = np.arange(20)
print(moving_averate(a, n=4))

print('37. 如何找到数组中出现次数最多的元素')
a = np.random.randint(0, 10, 50)
print(np.bincount(a).argmax())

print('38. 如何获取数组中最大的 n 个数')
a = np.arange(1000)
np.random.shuffle(a)
n = 5
start = time.time()
print('slow', a[np.argsort(a)[-n:]])
end = time.time()
print('slow time', end - start)
start = time.time()
print('fast', a[np.argpartition(-a, n)[:n]])
end = time.time()
print('fast time', end - start)


import numpy as np
import matplotlib.pyplot as plt

Notes_euler = '''
Euler's method:
    Function Name: Euler

    Inputs: f: defining function ( from y' = f(t,y) )
            a: starting Time
            b: ending Time
            y0: Initial Condition
            h: stepsize
            plot: Binary input
                Default plot = 0: No plotting
                        plot = 1: plots the solution

    Outputs: [t, y] where
        y: solution array
        t: time axis
'''

def Euler(f, a, b, y0, h, plot = 0):
    t = [a]
    y = [y0]
    N = int((b-a)/h)
    for i in range(N):
        y.append(y[-1] + h*f(t[-1], y[-1])) #euler update
        t.append(t[-1] + h) #time update

    if(plot == 1):
        plt.plot(t, y, label = "solution")
        plt.legend()
        plt.show()

    return [t, y]


from numpy import uint64, clip, searchsorted, bincount, maximum, minimum, diff, float64, zeros, equal, where, zeros_like

def cloud_in_cell(x,y, pts_x, pts_y, pts_vals=None, min_val=False, mean_val=True):
    """
    creates a grid of cells with borders x[:] and y[:]
    counts the fraction of particles in each cell, using cloud-in-cell approximation

    x - x borders
    y - y borders
    pts_x - x values of the points
    pts_y - y values of the points
    pts_vals - values for each point (default=None). If set then the grid contains the average value for the cells

    min_val=False - if True then set the minimum number in each cell as 1, (good for log plots)

    mean_val=True - if True then use the mean value for the cell (otherwise use the sum)

    """
    density = zeros((x.size*y.size), dtype=float64)
    if pts_vals is not None:
        values = zeros_like(density)

    # find the surrounding cell borders
    idx_x = searchsorted(x, pts_x)
    idx_x0 = maximum(idx_x-1, 0)
    idx_x1 = minimum(idx_x, x.size-1)

    dx = diff(x)[minimum(idx_x0, x.size-2)]
    frac_x = clip((pts_x - x[idx_x0]) / dx, 0, 1)

    idx_y = searchsorted(y, pts_y)
    idx_y0 = maximum(idx_y-1, 0)
    idx_y1 = minimum(idx_y, y.size-1)

    dy = diff(y)[minimum(idx_y0, y.size-2)]
    frac_y = clip((pts_y - y[idx_y0]) / dy, 0, 1)

    outside_box = equal(idx_x,0) | equal(idx_x,x.size) | equal(idx_y, 0) | equal(idx_y, y.size)

    idx00 = idx_y0 * x.size + idx_x0
    idx01 = idx_y0 * x.size + idx_x1
    idx10 = idx_y1 * x.size + idx_x0
    idx11 = idx_y1 * x.size + idx_x1

    w00 = where(outside_box, 0, (1-frac_y) * (1-frac_x))
    w01 = where(outside_box, 0, (1-frac_y) * frac_x)
    w10 = where(outside_box, 0, frac_y * (1-frac_x))
    w11 = where(outside_box, 0, frac_y * frac_x)

    for idx, weights in zip((idx00,idx01,idx10,idx11), (w00,w01,w10,w11)):
        b = bincount(idx, weights)

        density[:b.size] += b

        if pts_vals is not None:
            b = bincount(idx, weights * pts_vals)
            values[:b.size] += b

    res = density

    if min_val:
        res = maximum(res, 1.0)

    if pts_vals is not None:
        if mean_val:
            res = values / res
        else:
            res = values

        if min_val:
            res = maximum(res, pts_vals.min())
    else:
        # normalise
        res = res / res.sum()

    res = res.reshape((y.size, x.size))

    return res


import numpy as np

def regular_grid(ar_shape, n_points):
    """Find `n_points` regularly spaced along `ar_shape`.

    The returned points (as slices) should be as close to cubically-spaced as
    possible. Essentially, the points are spaced by the Nth root of the input
    array size, where N is the number of dimensions. However, if an array
    dimension cannot fit a full step size, it is "discarded", and the
    computation is done for only the remaining dimensions.

    Parameters
    ----------
    ar_shape : array-like of ints
        The shape of the space embedding the grid. ``len(ar_shape)`` is the
        number of dimensions.
    n_points : int
        The (approximate) number of points to embed in the space.

    Returns
    -------
    slices : list of slice objects
        A slice along each dimension of `ar_shape`, such that the intersection
        of all the slices give the coordinates of regularly spaced points.

    Examples
    --------
    >>> ar = np.zeros((20, 40))
    >>> g = regular_grid(ar.shape, 8)
    >>> g
    [slice(5.0, None, 10.0), slice(5.0, None, 10.0)]
    >>> ar[g] = 1
    >>> ar.sum()
    8.0
    >>> ar = np.zeros((20, 40))
    >>> g = regular_grid(ar.shape, 32)
    >>> g
    [slice(2.0, None, 5.0), slice(2.0, None, 5.0)]
    >>> ar[g] = 1
    >>> ar.sum()
    32.0
    >>> ar = np.zeros((3, 20, 40))
    >>> g = regular_grid(ar.shape, 8)
    >>> g
    [slice(1.0, None, 3.0), slice(5.0, None, 10.0), slice(5.0, None, 10.0)]
    >>> ar[g] = 1
    >>> ar.sum()
    8.0
    """
    ar_shape = np.asanyarray(ar_shape)
    ndim = len(ar_shape)
    unsort_dim_idxs = np.argsort(np.argsort(ar_shape))
    sorted_dims = np.sort(ar_shape)
    space_size = float(np.prod(ar_shape))
    if space_size <= n_points:
        return [slice(None)] * ndim
    stepsizes = (space_size / n_points) ** (1.0 / ndim) * np.ones(ndim)
    if (sorted_dims < stepsizes).any():
        for dim in range(ndim):
            stepsizes[dim] = sorted_dims[dim]
            space_size = float(np.prod(sorted_dims[dim+1:]))
            stepsizes[dim+1:] = ((space_size / n_points) **
                                 (1.0 / (ndim - dim - 1)))
            if (sorted_dims >= stepsizes).all():
                break
    starts = (stepsizes // 2).astype(int)
    stepsizes = np.round(stepsizes).astype(int)
    slices = [slice(start, None, step) for
              start, step in zip(starts, stepsizes)]
    slices = [slices[i] for i in unsort_dim_idxs]
    return slices


#!/usr/bin/env python3
# coding=utf-8

import numpy as np

zero_line = np.zeros((1, 3))
one_column = np.ones((3, 1))
print("zero_line = \n{}\n".format(zero_line))
print("one_column = \n{}\n".format(one_column))

a = np.array([(1, 2, 3), (4, 5, 6)])
b = np.arange(11, 20)
print("a = \n{}\n".format(a))
print("b = \n{}\n".format(b))

b = b.reshape(3, -1)
print("b.reshape(3, -1) = \n{}\n".format(b))

c = np.vstack((a, b, zero_line))
print("c = np.vstack((a,b, zero_line)) = \n{}\n".format(c))

a = a.reshape(3, 2)
print("a.reshape(3, 2) = \n{}\n".format(a))

d = np.hstack((a, b, one_column))
print("d = np.hstack((a,b, one_column)) = \n{}\n".format(d))

# np.vstack((a,b)) # ValueError: dimensions not match

e = np.hsplit(d, 3)  # Split a into 3
print("e = np.hsplit(d, 3) = \n{}\n".format(e))
print("e[1] = \n{}\n".format(e[1]))

# np.hsplit(d, 4) # ValueError: array split does not result in an equal division

f = np.hsplit(d, (1, 3))  # # Split a after the 1st and the 3rd column
print("f = np.hsplit(d, (1, 3)) = \n{}\n".format(f))

g = np.vsplit(d, 3)
print("np.vsplit(d, 3) = \n{}\n".format(g))

# np.vsplit(d, 2) # ValueError: array split does not result in an equal division


#taken from https://machinelearningmastery.com/statistical-hypothesis-tests-in-python-cheat-sheet/
#This section lists statistical tests that you can use to check if two samples are related.

from scipy.stats import pearsonr
def pearson_coeff(data1, data2) -> (float, float):
    """Tests whether two samples have a linear relationship.
    Assumptions
    -----------
    * Observations in each sample are independent and identically distributed (iid).
    * Observations in each sample are normally distributed.
    * Observations in each sample have the same variance.
    Interpretation
    --------------
    * H0: the two samples are independent.
    * H1: there is a dependency between the samples.

    Parameters
    ----------
    data1: (N,) array_like
    data2: (N,) array_like

    Returns
    -------
    corr : float
        Pearson's correlation coefficient
    p : float
        2-tailed p-value
    """
    corr, p = pearsonr(data1, data2)
    return corr, p

from scipy.stats import spearmanr
def spearman_rank(data1, data2):
    """Tests whether two samples have a monotonic relationship.
    Assumptions
    -----------
    * Observations in each sample are independent and identically distributed (iid).
    * Observations in each sample can be ranked.
    Interpretation
    --------------
    * H0: the two samples are independent.
    * H1: there is a dependency between the samples.

    Parameters
    ----------
    data1: (N,) array_like
    data2: (N,) array_like

    Returns
    -------
    corr : float or ndarray (2-D square)
        Spearman correlation matrix or correlation coefficient (if only 2
    variables are given as parameters. Correlation matrix is square with
    length equal to total number of variables (columns or rows) in `a`
    and `b` combined.
    p : float
        The two-sided p-value for a hypothesis test whose null hypothesis is
    that two sets of data are uncorrelated, has same dimension as rho.
    """
    corr, p = spearmanr(data1, data2)
    return corr, p

from scipy.stats import kendalltau
def kendall_rank(data1, data2) -> (float, float):
    """Tests whether two samples have a monotonic relationship.
    Assumptions
    -----------
    * Observations in each sample are independent and identically distributed (iid).
    * Observations in each sample can be ranked.
    Interpretation
    --------------
    * H0: the two samples are independent.
    * H1: there is a dependency between the samples.

    Parameters
    ----------
    data1: (N,) array_like
    data2: (N,) array_like

    Returns
    -------
    corr : float
        The tau statistic.
    p : float
        The two-sided p-value for a hypothesis test whose null hypothesis is an absence of association, tau = 0.
    """
    corr, p = kendalltau(data1, data2)
    return corr, p

from scipy.stats import chi2_contingency
def chi_squared(table):
    """Tests whether two categorical variables are related or independent.
    Assumptions
    -----------
    * Observations used in the calculation of the contingency table are independent.
    * 25 or more examples in each cell of the contingency table.
    Interpretation
    --------------
    * H0: the two samples are independent.
    * H1: there is a dependency between the samples.

    Parameters
    ----------
    table: array_like

    Returns
    -------
    stat : float
        The test statistic.
    p : float
        The p-value of the test
    dof : int
        Degrees of freedom
    expected : ndarray, same shape as observed
        The expected frequencies, based on the marginal sums of the table.
    """
    stat, p, dof, expected = chi2_contingency(table)
    return stat, p, dof, expected


#! /usr/bin/env python

"""
File: find_primes.py
Copyright (c) 2016 Chinmai Raman
License: MIT

Course: PHYS227
Assignment: 1.4
Date: Feb 12, 2016
Email: raman105@mail.chapman.edu
Name: Chinmai Raman
Description: The Sieve of Eratosthenes. Finds all prime numbers less than or equal to a given number N.
"""

import numpy as np

primes = []

def sieve(N):
    """
    Finds all prime numbers less than or equal to a given number N.
    """
    for i in range(2, N + 1):
        primes.append(i)
    p = 2
    while (p < N):
        i = 2
        while((p * i) < (N + 1)):
            if ((p * i in primes) == True):
                del primes[primes.index(p * i)]
            i += 1
        p += 1

def test():
    sieve(10)
    assert([2, 3, 5, 7] == primes), "Failure."


"""Tool for finding the eigenvalues using Jacobi eigenvalue algorithm

Functions:
    calculate_eigenvalues(A, eps) -> ndarray
"""

from numpy import diag, diag_indices, argmax, unravel_index, ones, sign, dot

def calculate_eigenvalues(A, eps):
    """Find the eigenvalues of matrix with a given accuracy.

    Arguments:
    A -- symmetric matrix
    eps -- accuracy
    """

    n = A.shape[0]
    di = diag_indices(n)
    zeroed_diag_A = A.copy()
    zeroed_diag_A[di] = 0
    while True:
        indices = unravel_index(argmax(abs(zeroed_diag_A), axis=None), zeroed_diag_A.shape)
        i, j = min(indices), max(indices)

        T_ij = diag(ones(n))
        d = ((A[i, i] - A[j, j]) ** 2 + 4 * A[i,j] ** 2) ** 0.5
        T_ij[i, i] = (((1 + abs(A[i, i] - A[j, j]) / d)) / 2) ** 0.5
        T_ij[j, j] = T_ij[i, i]
        T_ij[j, i] = sign(A[i, j] * (A[i, i] - A[j, j])) * (((1 - abs(A[i, i] - A[j, j]) / d)) / 2) ** 0.5
        T_ij[i, j] = - T_ij[j, i]

        A = dot(dot(T_ij.T, A), T_ij)

        zeroed_diag_A = A.copy()
        zeroed_diag_A[di] = 0
        if sum(sum(zeroed_diag_A ** 2)) < eps:
            break

    return A[di]

def calculate_eigenvalues_m(A, eps):
    """Find the eigenvalues of matrix with a given accuracy.

    This function just uses formulas without using matrix multiplication

    Arguments:
    A -- symmetric matrix
    eps -- accuracy
    """

    n = A.shape[0]
    di = diag_indices(n)
    zeroed_diag_A = A.copy()
    zeroed_diag_A[di] = 0
    while True:
        indices = unravel_index(argmax(abs(zeroed_diag_A), axis=None), zeroed_diag_A.shape)
        i, j = min(indices), max(indices)

        d = ((A[i, i] - A[j, j]) ** 2 + 4 * A[i,j] ** 2) ** 0.5
        cosfi = (((1 + abs(A[i, i] - A[j, j]) / d)) / 2) ** 0.5
        sinfi = sign(A[i, j] * (A[i, i] - A[j, j])) * (((1 - abs(A[i, i] - A[j, j]) / d)) / 2) ** 0.5

        C = A.copy()
        C[i, i] = (A[i, i] + A[j, j]) / 2 + sign(A[i, i] - A[j, j]) * d / 2
        C[j, j] = (A[i, i] + A[j, j]) / 2 - sign(A[i, i] - A[j, j]) * d / 2
        C[i, j], C[j, i] = 0, 0
        for k in range(n):
            if k == i or k == j:
                continue

            C[i, k] = cosfi * A[k, i] + sinfi * A[k, j]
            C[k, i] = C[i, k]
            C[j, k] = - sinfi * A[k, i] + cosfi * A[k, j]
            C[k, j] = C[j, k]

        A = C
        zeroed_diag_A = A.copy()
        zeroed_diag_A[di] = 0
        if sum(sum(zeroed_diag_A ** 2)) < eps:
            break

    return A[di]


#=============================================================================#
#
#    File:   euler_product_formula.py
#    Author: Jack Morgan
#    Date:   May 2021
#    Description:
#        Calculate zeta by a product of primes
#        Where \zeta(s) = \prod_{p, prime} \frac{1}{1-p^{^{-s}}}
#
#=============================================================================#

from time import time
import numpy as np
from math import floor, sqrt, pi

def sieve_of_eratosthenes(limit):
    possible_primes = np.ones(limit, dtype=bool)
    possible_primes[0:2:1] = False
    for i in range(2, floor(sqrt(limit))):
        possible_primes[i*i:limit:i] = False
    return np.flatnonzero(possible_primes)

def zeta(s):
    limit = 1*10**6
    primes = sieve_of_eratosthenes(limit).astype(float)
    elements = 1/(1-primes**(-s))
    return np.prod(elements)

def test():
    TOL = 1e-3

    test_case_1 = zeta(3.0 + 4.5 * 1j)
    test_case_2 = zeta(1 - 2.5 * 1j)
    test_case_3 = zeta(2 + 0 * 1j)

    # true values taken from wolfram alpha
    test1 = abs(test_case_1.real - 0.89796690) < TOL and abs(test_case_1.imag - 0.023707555) < TOL
    test2 = abs(test_case_2.real - 0.61134995) < TOL and abs(test_case_2.imag - 0.213320800) < TOL
    test3 = abs(test_case_3.real - pi**2/6) < TOL and abs(test_case_3.imag - 0.000000000) < TOL

    print(f'test1: {test1}\ntest2: {test2}\ntest2: {test3}')

if __name__=='__main__':
    start = time()
    test()
    print(f'--- {time()-start} seconds ---')


import numpy as np
from itertools import permutations as perm
import pdb

def compute_present_value(j,I,p):
    """ Computes Present Value (PV) of an ordinary annuity """

    # NOTES:
    # Any ONE of the input arguments can be an array, but the others
    # must be scalar.

    # INPUTS:
    # p -> Periodic payment amount
    # I -> Interest, as annual rate. If 9%, enter 0.09.
    # j -> The number of payments

    # OUTPUT:
    # PV -> The present value of the annuity

    PV = p * ( ( 1 - (1+I)**(-j) )/I )

    return PV

def compute_future_value(j,I,p):
    """ Computes Future Value (FV) or an ordinary annuity """

    # NOTES:
    # Any ONE of the input arguments can be an array, but the others
    # must be scalar.

    # INPUTS:
    # p -> Periodic payment amount
    # I -> Interest, as annual rate. If 9%, enter 0.09.
    # j -> The number of payments

    # OUTPUT:
    # FV -> The future value of the annuity

    FV = p * (((1+I)**j - 1)/I) * (1+I)

    return FV

def compute_monthly_loan_payment(PV,I,j):
    """ Computes the monthly payment on a loan given neccessary inputs """

    # INPUTS:
    # PV -> Present value ($)
    # I -> Interest, as annual rate. If 9%, enter 0.09.
    # j -> Number of payments

    # [10/8/18] PV ordinary annuity only

    p = PV / ( ( 1 - (1+I)**(-j) )/I )

    return p

def compute_n_payments(L0, I, p):
    """ Compute the number of payments required to pay a loan off to a
        balance of zero.
        Any ONE of the input arguments can be an array, but the others
        must be scalar.
    """

    # INPUTS:
    # L0 -> The initial loan amount borrowed.
    # I -> Interest, as annual rate. If 9%, enter 0.09.
    # p -> The periodic payment amount

    # OUTPUT:
    # n -> number of payments, either a single number (if all inputs are
    #      scalars), or an array of n's (if one of the inputs is an array)

    # Eq. for # of payments, n, as a function of payment amount, p
    n = -1*( np.log( 1 - ((L0*I)/p) )  /  np.log(1+I) )
    return n

def compute_total_cost(L, I, p, ndec, ntsteps=0):
    """ This function uses an old-fashioned loop to compute the total cost
        of a loan.
    """

    # INPUTS:
    # L      -> current inital/principal of the loan
    # I      -> interest (presumably annual) of the loan
    # p      -> the payment at each timestep (presumably monthly)
    # nsteps -> optional. compute the total cost only out to this many timesteps,
    #           except if = 0. in this case, compute total cost for entire loan
    #           lifetime (default)
    # If you set ntsteps != 0 then only the total out to nsteps will be
    # computed.

    # OUTPUTS:
    # total  -> the total cost of the loan
    # tsteps -> the total number of timesteps it took to pay off the loan
    # L      -> the running principal of the loan, which is returned in its last
    #           state, representing the amount leftover as a negative

    # NOTES:
    # [1/30/19] All inputs are assumed to be scalar.

    total = 0.0
    tsteps = 0
    leftover = 0.0

    while L > 0:
        leftover = 0.0
        L = np.round( L + np.round(L*I,ndec) - p, ndec )
        total = total + p
        if L < 0: leftover = abs(L)
        tsteps += 1
        if ntsteps == tsteps: break

    total = total - leftover

    return total, tsteps, L

def descending_interest_method(L0, I, p0, a0, ndec):
    """ This function will return the total cost using the method of
        paying off loans in order of descending interest.
    """

    # INPUTS:
    # L0   -> Current inital/principal of the loans [1D array]
    # I    -> Interest (presumably annual) of the loans [1D array]
    # p    -> The payment at each timestep (presumably monthly) [1D array]
    # a0   -> The extra money available each month to put towards max(I) loan (scalar)
    # ndec -> The number of decimal places to round to - presumably 2, cuz money. (scalar)

    # OUTPUTS:
    # (all scalars)
    # grand_total -> The total cost of paying off all loans to a balance of
    #                zero using this method.
    # tsteps      -> The number of timesteps taken to pay off all loans.

    # NOTES:
    #
    # [1/21/19]
    # After each payoff, leftover is applied to next iteration.
    # Payment is kept constant at every iteration, save any leftover from
    # previous iteration. So, even after a loan is paid off, the code
    # continues to use that loans minimum payment to pay off
    # remaining loans.

    # Test if all values of I are equal
    if (I == np.roll(I,1)).sum() == I.shape[0]:
        #print("!!!! WARNING !!!!")
        #print("Descending interest method results not meaningful in case")
        #print("where all interest rates are the same.")
        #print("!!!!!!!!!!!!!!!!")
        pass

    L = np.copy(L0)
    p = np.copy(p0)
    nL = L0.shape[0]
    includeLmask = np.ones(nL,dtype=np.bool)
    payedoff_mask = np.zeros(nL,dtype=np.bool)
    a = np.zeros(nL,dtype=np.float64)
    total = 0.0
    rem  = L0.sum()
    tsteps = 0
    leftover = 0.0

    while rem > 0:
        maxI_indx = I[includeLmask].argmax()
        a[:] = 0
        a[maxI_indx] = a0 + leftover + p0[payedoff_mask].sum()
        leftover = 0.0
        L = np.round( L + np.round(L*I,ndec) - (p + a), ndec )
        payedoff_mask = L <= 0.0
        if payedoff_mask.sum() > 0:
            leftover = np.abs(L[payedoff_mask].sum())
            includeLmask[payedoff_mask] = False
            p[payedoff_mask] = 0.0
            L[payedoff_mask] = 0.0
        total = total + p0.sum() + a0
        rem = L.sum()
        tsteps += 1

    grand_total = total - leftover

    return grand_total, tsteps

def gradient_descent_algo1(L0, I, p0, a, max_iters, x = 0.01):
    """ This algorithm determines the weights and the grand total of
        applying those weights to an amount, a, over the minimum payment
        of an arbitrary number of fixed interest annuities (loans).
        The way the weights are determined is described in the DESCRIPTION
        below.
    """

    # DESCRIPTION:
    # This algorithm repeatedly checks which loan's grand total cost is
    # reduced the most by applying the same amount over the minimum fixed
    # payment (a) to each loan. Let's call this loan the "winner."
    # At the end of each iteration, the winner's payment amount is increased
    # by x (fraction of 1, input, defined below). The next iteration begins.
    # Iterations continue until 100% of "a" (input, defined below) is allocated.
    # The winner will sometimes change as the payments change, as the code
    # iterates. At the end of iterations, you're left with an array that
    # contains the "optimal" fractions (called weights in output) of "a"
    # to apply to each of the loans.
    # [5/17/20] Like "descending_interest_method" function...
    # Payment is kept constant at every iteration, save any leftover from
    # previous iteration. So, even after a loan is paid off, the code
    # continues to use that loan's minimum payment to pay off
    # remaining loans.

    # INPUTS:
    # L0        -> The initial principal loan amount [numpy 1D array]
    # I         -> The interest [numpy 1D array]
    # p0        -> The minimum payment amounts [numpy 1D array]
    # a         -> extra amount over the minimum payments willing to be paid [scalar]
    # max_iters -> maximum iterations to try allocating a [scalar]
    # x         -> fraction by which to increment weights [scalar]

    # OUTPUTS:
    # w -> the weights optimizing the allocation of a to each loan [numpy 1D array]
    # n -> the resultant number of payments made for each loan [numpy 1D array]
    # grand_total_paid -> the resultant grand total paid [scalar]

    p = np.copy(p0)
    nL = L0.shape[0]
    w = np.zeros(nL)
    delta = np.zeros(nL)
    j = 0
    wrem = 1.0 # represents the remainding % of 'a' to allocate

    while (wrem > 0.0):
        delta_last = 0.0
        isave = None
        for i in range(len(L0)):
            n0 = compute_n_payments(L0[i], I[i], p[i])
            t0 = n0 * p[i]
            pmod = p[i] + x*a
            n1 = compute_n_payments(L0[i], I[i], pmod)
            t1 = n1 * pmod
            delta[i] = t0 - t1 # diff in totals b4 & after modification
            if delta[i] > delta_last:
                isave = i
            delta_last = delta[i]
        if isave is None:
            pdb.set_trace()
        else:
            wrem = wrem - x
        w[isave] = w[isave] + x
        p[isave] = p[isave] + x*a
        if j > max_iters:
            print('Max iterations reached...')
            pdb.set_trace()
            break
        j += 1

    paid = []
    n = []
    for i in range(len(L0)):
        nt = compute_n_payments(L0[i], I[i], p0[i]+w[i]*a)
        paid.append(p[i] * nt)
        n.append(nt)
    grand_total_paid = sum(paid)
    return w, np.asarray(n), grand_total_paid

def optimize_algo1(L0, I, p, a0, max_iters, ndec, x = 0.01):
    """ Uses  gradient_descent_algo1 to compute total loan cost """

    # DESCRIPTION:
    # The "gradient_descent_algo1" function defined above is run using
    # the input loans. This yields a somewhat optimized array of weights
    # and # of timesteps (n). These are the timesteps required to payoff
    # each of the input loans. The total cost is computed by paying all
    # loans to n.min() timesteps. Unless every element of n is the same,
    # you'll have a new set of loans to optimize paying off, each time you
    # you reach the next highest n. Therefore "gradient_descent_algo1"
    # is called iteratively in a 'while' loop until the principal
    # of all remaining loans goes to zero.

    # INPUTS:
    # L0        -> The initial principal loan amount [numpy 1D array]
    # I         -> The interest [numpy 1D array]
    # p0        -> The minimum payment amounts [numpy 1D array]
    # a         -> extra amount over the minimum payments willing to be paid [scalar]
    # max_iters -> maximum iterations to try allocating a [scalar]
    # ndec      -> number of decimal places to round to when computing total cost [scalar]
    # x         -> fraction by which gradient_descent_algo1 should increment weights [scalar]

    # OUTPUTS:
    # tot_all     -> total amount paid [scalar]
    # tot_ts      -> total number of timesteps taken to payoff all loans [scalar]
    # all_w       -> list of numpy arrays containing the "best" weights for each iteration
    # all_ntsteps -> list of timesteps taken at each iteration

    # NOTES:
    #
    # [1/21/19]
    # Leftover gets applied on next iteration.
    # [4/19/20]
    # Patch put in for condition where >= 2 loans are zero and rest are neg.

    nL = L0.shape[0]
    L = np.copy(L0)
    mask = np.ones(nL,dtype=np.bool)
    tot_all = 0.0
    tot_ts = 0
    a = a0
    leftover = 0.0
    all_w = []      # all weights, for all iterations
    all_ntsteps = []# all num of tsteps, for all iterations

    while (mask.sum() > 1):

        nloans_toploop = mask.sum()
        L = L[mask]
        I = I[mask]
        p = p[mask]
        #print("This many L remain: ",L.shape)

        # IF the remainder to be paid on all loans is less than leftover,
        # or the minimum payments, then quit. But be sure to increase
        # loans by 1 ts worth of I before making this comparison.
        L_nts = np.round( L + np.round(L*I,ndec), ndec)
        if (L_nts.sum() < a) | ((L_nts >= p).sum() == 0):
            # First subtract the minimum payments from the remaining loan
            # amounts. The results will likely be negative.
            payment_step = L_nts - p
            # If min payments weren't enough, then apply extra amount 'a'
            after_payment = payment_step.sum()
            if after_payment > 0:
                leftover = a - after_payment
            else: # <= 0
                leftover = a + np.fabs(after_payment)
            L = L*0.0
            mask = np.zeros(L.shape, dtype=np.bool)
            break # done with this while loop

        w, n, grand_total_paid = gradient_descent_algo1(L, I,
                                 p, a, max_iters, x)

        n = np.ceil(n) # round up to nearest int
        ntsteps = n.min()
        all_ntsteps.append(ntsteps)
        tot_ts = tot_ts + ntsteps
        all_w.append(w)

        for i in range(0,nloans_toploop):
            tot, ts, Lout = compute_total_cost(L[i], I[i], p[i]+a*w[i], ndec, ntsteps)
            tot_all = tot_all + tot
            L[i] = Lout

        mask = L >= 0
        # Put a patch in here so that if every value in L is <= 0, mask.sum() == 0.
        # This will terminate while loop once it gets back to top.
        if (L > 0).sum() == 0:
            mask = np.zeros(mask.shape,dtype=np.bool)
        invmask = L < 0
        leftover = np.abs(L[invmask]).sum()
        a = p[invmask].sum() + a0 + leftover # keeps total payment amount constant

    # Compute cost of paying off rest of the remaining loan, if applicable
    if mask.sum() > 0:
        L = L[mask]
        I = I[mask]
        p = p[mask]
        tot, ts, Lout = compute_total_cost(L[0], I[0], p[0]+a, ndec)
        tot_all = tot_all + tot + Lout # Lout should be neg here
        tot_ts = tot_ts + ts
        all_ntsteps.append(ts)
        all_w.append(1.0)
    else:
        # At least some of the loans must've been paid off in same number
        # of times steps.
        tot_all = tot_all - leftover

    return tot_all, tot_ts, all_w, all_ntsteps

def partition_permutations(number):
    """ Compute the permutations of the partitions of the input number.
    """
    answer = set()
    answer.add((number, ))
    for x in range(1, number):
        for y in partition(number - x):
            answer.add((x, ) + y)
    return answer

def partition(number):
    """ Compute the of the partitions of the input number.
    """
    answer = set()
    answer.add((number, ))
    for x in range(1, number):
        for y in partition(number - x):
            answer.add(tuple(sorted((x, ) + y)))
    return answer

def gradient_descent_algo2(L0, I, p, a, n, m):
    """ This algorithm finds the m lowest costs produced by running weights
        created using combinatorics and permutations to test the full range
        of possibilities.
    """

    # DESCRIPTION:
    # This algorithm

    # INPUTS:
    # L0        -> The initial principal loan amount [numpy 1D array]
    # I         -> The interest [numpy 1D array]
    # p         -> The minimum payment amounts [numpy 1D array]
    # a         -> extra amount over the minimum payments willing to be paid [scalar]
    # n         -> defines the resolution of the combinatorics
    # m         -> the m lowest total cost combinations will be returned

    # OUTPUTS:
    # w                -> the weights resulting in the m lowest total costs [numpy array]
    # n_pay            -> the resultant number of payments made for each
    #                     loan, corrresponding to w [numpy array]
    # grand_total_paid -> the resultant grand totals paid, corresponding to w [numpy array]

    # Google: partitions, compositions, number theory, combinatorics
    #
    # NOTES:
    # [5/25/20]
    # Code abandoned indefinitely because it takes a long time to run.

    nL = L0.shape[0]

    partitions = list(partition(n))
    # pared_partitions will be a list of tuples. Each tuple will be a set
    # of no more than nL numbers that add up to n. This set should include
    # every possible combo of nL (or less) numbers that sums to n.
    pared_partitions = list(filter(lambda x: len(x) <= nL, partitions))
    samp_arr = np.zeros(nL,dtype=np.float64)

    m_lowest_costs = []
    n_of_low_costs = []
    tup_of_low_costs = []

    for pt in pared_partitions:
        samp_arr[:] = 0.0
        partlen = len(pt)
        samp_arr[:partlen] = np.array(pt)
        all_vals_eq = (samp_arr[1:] == samp_arr[:-1]).sum() == (nL-1)
        # At least get rid of the repeats in cases where every value is equal
        if all_vals_eq:
            permu_tup_list = [tuple(samp_arr)]
        else:
            permu_tup_list = list(perm(samp_arr))
        for tup in permu_tup_list:
            i = 0
            tot_cost = 0.0
            n_pay_list = []
            for val in tup:
                w = float(val)/float(n)
                pmod = p[i]+w*a
                n_pay = compute_n_payments(L0[i], I[i], pmod)
                total = n_pay * pmod
                tot_cost = tot_cost + total
                n_pay_list.append(n_pay)
                i += 1
            # Keep m lowest values in m_lowest_costs list.
            if len(m_lowest_costs) >= m:
                list_max = max(m_lowest_costs)
                if tot_cost < list_max:
                    max_indx = m_lowest_costs.index(list_max)
                    m_lowest_costs[max_indx] = tot_cost
                    n_of_low_costs[max_indx] = n_pay_list
                    tup_of_low_costs[max_indx] = tup
                    #print(tot_cost)
                    #print(m_lowest_costs)
                    #print(n_of_low_costs)
                    #print(tup_of_low_costs)
                    #pdb.set_trace()
            else:
                m_lowest_costs.append(tot_cost)
                n_of_low_costs.append(n_pay_list)
                tup_of_low_costs.append(tup)
            if i < nL:
                pdb.set_trace()

    w = np.array(tup_of_low_costs,dtype=np.float64)
    w = w / float(n)
    n_pay = np.array(n_of_low_costs)
    grand_total_paid = np.array(m_lowest_costs)
    return w, n_pay, grand_total_paid


"""

A simple optimal savings problem.  The Bellman equation is

    v(x, z) = max_x' { u(R x + w z - x') + β E v(x', z')}

where 0 <= x' <= R x + w z and

    E v(x', z') = Σ_{z'} v(x', z') Q(z, z')

We take

    u(c) = c^{1 - γ} / (1 - γ)

and obtain the transition kernel p by discretizing

    log z' = ρ log z + d + σ η

using Rouwenhorst's method.

"""

import numpy as np
import quantecon as qe
from numba import jit, prange

@jit(nopython=True)
def u(c, γ):
    return (c + 1e-10)**(1 - γ) / (1 - γ)

class SavingsProblem:

    def __init__(self,
                 β=0.96,
                 γ=2.5,
                 ρ=0.9,
                 d=0.0,
                 σ=0.1,
                 r=0.04,
                 w=1.0,
                 z_grid_size=25,
                 x_grid_size=200,
                 x_grid_max=15):

        self.β, self.γ = β, γ
        self.R = 1 + r
        self.w = w
        self.z_grid_size, self.x_grid_size = z_grid_size, x_grid_size

        mc = qe.rouwenhorst(z_grid_size, d, σ, ρ)
        self.Q = mc.P
        self.z_grid = np.exp(mc.state_values)

        self.x_grid = np.linspace(0.0, x_grid_max, x_grid_size)

    def pack_parameters(self):
        return self.β, self.γ, self.R, self.w, self.Q, self.x_grid, self.z_grid

    def value_function_iteration(self,
                                tol=1e-4,
                                max_iter=1000,
                                verbose=True,
                                print_skip=25):

        # Set initial condition, set up storage
        v_in = np.ones((self.x_grid_size, self.z_grid_size))
        v_out = np.empty_like(v_in)
        π = np.empty_like(v_in, dtype=np.int)

        # Set up loop
        params = self.pack_parameters()
        i = 0
        error = tol + 1

        while i < max_iter and error > tol:
            T(v_in, v_out, π, params)
            error = np.max(np.abs(v_in - v_out))
            i += 1
            if i % print_skip == 0:
                print(f"Error at iteration {i} is {error}.")
            v_in[:] = v_out

        if i == max_iter:
            print("Failed to converge!")

        if verbose and i < max_iter:
            print(f"\nConverged in {i} iterations.")

        return v_out, π

@jit(nopython=True, parallel=True)
def T(v, v_out, π, params):
    """
    Given v, compute Tv and write it to v_out.

    At the same time, compute the v-greedy policy and write it to π

    """
    n, m = v.shape
    β, γ, R, w, Q, x_grid, z_grid = params

    for j in prange(m):
        z = z_grid[j]

        for i in range(n):
            x = x_grid[i]

            # Cash in hand at start of period
            y = R * x + w * z
            # A variable to store largest recorded value
            max_so_far = - np.inf

            # Find largest x_grid index s.t. x' <= y
            idx = np.searchsorted(x_grid, y)

            # Step through x' with 0 <= x' <= y, find max
            for k in range(idx):
                x_next = x_grid[k]
                val = u(y - x_next, γ) + β * np.sum(v[k, :] * Q[j, :])
                if val > max_so_far:
                    max_so_far = val
                    k_star = k

            π[i, j] = k_star
            v_out[i, j] = max_so_far


import numpy as np
import gd
import matplotlib.pyplot as plt

m = 15
# X: m x 2 matrix, X[i,:] is the 2D feature vector of the i-th sample
X = np.array([[1, 1.5],
              [1.2, 2.5],
              [1, 3.5],
              [2, 2.25],
              [1.8, 3],
              [2.5, 4],
              [3, 1.9],
              [1.5, .5],
              [2.5, .8],
              [2.8, .3],
              [3.2, .3],
              [3, .8],
              [3.8, 1],
              [4, 2],
              [1.8, 1.8]])
# y: m-D vector, y[i] is the label of the i-th sample
y = np.append(np.ones((7,)), -np.ones((8,)))

# append a constant 1 to each feature vector, so X is now a m x 3 matrix
X = np.append(X, np.ones((15, 1)), axis=1)

# Xy[i,:] = X[i,:] * y[i]
Xy = X * y.reshape((-1, 1))

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

def fp(w):
    s = np.array([0.0, 0.0, 0.0])
    for i in range(m):
        s += (sigmoid(y[i] * (X[i, :].T @ w)) - 1) * y[i] * X[i, :]
    return s.reshape(-1, 1)

stepsize = 0.1
maxiter = 1000000
w0 = np.array([[0.0], [0.0], [0.0]])
w_traces = gd.gd_const_ss(fp, w0, stepsize=stepsize, maxiter=maxiter)

print(
    f'stepsize={stepsize}, number of iterations={len(w_traces)-1}')
print(f'optimal solution:\n{w_traces[-1]}')

# compute the accuracy on the training set
w = w_traces[-1]
y_hat = 2*(X@w > 0)-1

accuracy = sum(y_hat.T[0] == y) / float(len(y))
print(f"accuracy = {accuracy}")

# visualization
plt.figure(figsize=(4, 4))

plt.scatter(*zip(*X[y > 0, 0:2]), c='r', marker='x')
plt.scatter(*zip(*X[y < 0, 0:2]), c='g', marker='o')

# plot the decision boundary w[0] * x1 + w[1] * x2 + w[0] = 0
x1 = np.array([min(X[:, 0]), max(X[:, 0])])
x2 = -(w[0] * x1 + w[2]) / w[1]
plt.plot(x1, x2, 'b-')

plt.xlabel('x1')
plt.ylabel('x2')

plt.tight_layout()
plt.savefig('../figures/p3.pdf')


import numpy as np
from sympy import *
import matplotlib.pyplot as plt
import time
from math import *
from scipy.misc import derivative
import scipy.integrate as integrate

global iteration

def newtonTrapezoidal(f,a,b,n):
    array = []
    h=(b-a)/n
    trapezoidalSum=0
    part1=(0.5)*h*(f(a)+f(b))
    for i in range(1,n):
        xi=a+i*h
        trapezoidalSum=trapezoidalSum+f(xi)
        array.append(part1+h*trapezoidalSum)

    plt.plot(array, 'k--', label="Trapezoidal")
    plt.legend(loc='best')

    print('Trapezoidal result: ' + str(part1+h*trapezoidalSum))

    h = (b - a) / n
    c = derivative(f, float(a + b) / 2, n=2)
    error = (((b - a) ** 2) * (h ** 2) * c) / 25

    print('Trapezoidal error: ' + str(error))

def newtonSimpson(f, a, b, n):
    if a > b:
        print('Incorrect bounds')
        return None
    else:
        if n%2 != 0:
            n = n+1
        h = (b - a)/float(n)
        sum1 = 0
        for i in range(1, int(n/2 + 1)):
            sum1 += f(a + (2*i - 1)*h)
        sum1 *= 4
        sum2 = 0
        for i in range(1, int(n/2)):
            sum2 += f(a + 2*i*h)

        sum2 *= 2
        approx = (b - a)/(3.0*n)*(f(a) + f(b) + sum1 + sum2)
        print ('Simpson result: ' + str(approx))

        actual = integrate.quad(f, 0, 1)[0]
        error = round(abs(actual - approx), 8)
        print ('Simpson error: ' + str(error))

def newtTrapSimp(func, a, b, n):
    #### SIMPSON
    newtonSimpson(func, a, b, n)

    ### TRAPEZOIDAL
    newtonTrapezoidal(func, a, b, n)

if __name__ == '__main__':
    func = lambda x: x**2
    a = 0.0
    b = 1.0
    n = 4

    newtTrapSimp(func, a, b, n)

import numpy,import scipy,from scipy,from sympy


"""
Input: An example problem using a deck of cards will be used to demonstrate definition of the terms
What is the probability of receiving 2 Kings from a deck of 52 playing cards (containing 4 Kings)
when 6 cards are drawn from the deck
type_threshold is the number of a specific item from which the probability is calculated (e.g. 2 Kings)
no_of_type_in_set is the number of the specific item found amongst the total number of items (e.g.4 Kings within the deck of 52 playing cards)
sample_size is the set of items that are drawn from the total (e.g. 6 cards drawn from the deck )
set_size is the total number of the items in the set (e.g. the 52 cards in a deck of playing cards)

@author: kevoulee
"""

from IPython import get_ipython
get_ipython().magic('clear')
#get_ipython().magic('reset -sf')

import numpy as np
import math
import operator
from decimal import Decimal

def nCk(n,k):
    if (n - k) < 0 or n < 0 or k < 0:
        return 0;
    return np.float64((Decimal(math.factorial(n)) / Decimal(math.factorial(n - k))) / Decimal(math.factorial(k)))

def probability(type_threshold, no_of_type_in_set, sample_size, set_size) -> np.float64:
    #Type checking - Python does not have built-in type checking
    type_error = False
    ##type_threshold
    try:
        operator.index(type_threshold)
        T = np.int_(type_threshold)
    except TypeError:
        print('Error: \'type_threshold\' - expected int (or int variant), received %s' % (type(type_threshold)))
        type_error = True

    ##no_of_type_in_set
    try:
        operator.index(no_of_type_in_set)
        m = np.int_(no_of_type_in_set)
    except TypeError:
        print('Error: \'no_of_type_in_set\' - expected int (or int variant), received %s' % (type(no_of_type_in_set)))
        type_error = True

    ##sample_size
    try:
        operator.index(sample_size)
        n = np.int_(sample_size)
    except TypeError:
        print('Error: \'sample_size\' - expected int (or int variant), received %s' % (type(sample_size)))
        type_error = True

    ##set_size
    try:
        operator.index(set_size)
        N = np.int_(set_size)
    except TypeError:
        print('Error: \'set_size\' - expected int (or int variant), received %s' % (type(set_size)))
        type_error = True

    if type_error:
        return 0

    #Calculation
    P_tot = 0
    #Calculate probability
    for i in range(T, n + 1):
        P_tot = P_tot + nCk(m, i) * nCk(N - m, n - i) / nCk(N, n)
    return P_tot


#!/usr/bin/python3.9
# Licence MIT copyright (c) 2022 Stéphane Lassalvy
import numpy as np
def my_CribleEratosthene(n):
    # Algorithme du crible d'Eratostène pour calculer les nombres premiers inférieur ou égaux à un entier naturel n
    # Il est basé sur le principe qu'un nombre est premier s'il n'est divisible par aucun des nombres premiers qui le précèdent.
    print("On peut considérer par convention que 1 est un nombre premier.")
    nombrePremiers = [2]
    # i est un candidat à être un nombre premier
    i = 3
    while i <= n:
        restes = []
        # Le nombre i est premier s'il n'est pas divisible par les nombre premiers qui le précédent.
        # Pour le savoir, on établit alors la liste des restes des divisions euclidiennes de i par les nombres premiers qui le précèdent.
        for j in nombrePremiers:
            restes.append(i % j)
        # Une fois qu'on a calculé tous les restes, on regarde le reste le plus petit, et s'il est différent de 0,
        # c'est que tous les restes sont différent de 0, et donc que i n'est divisible par aucun des nombres premiers qui le précèdent.
        # On rajoute dans ce cas i à la liste des nombres premiers connus
        if min(restes) > 0:
            nombrePremiers.append(i)
        # et on passe à l'entier i suivant, on ajoute 2 à i car il est inutile de tester les multiples de 2
        i = i + 2
    print(f"Liste des nombres premiers compris dans l'intervalle [2; {n}]:")
    print(nombrePremiers)
# Exécution du programme
n = input("Indiquez le nombre n pour lequels vous voulez calculez les nombres premiers <= n :\n")
n = int(n)
my_CribleEratosthene(n)


# Chapter 9

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

## Review

[In the previous notebook](https://colab.research.google.com/github/AllenDowney/BiteSizeBayes/blob/master/08_soccer.ipynb) I introduced the World Cup problem:

>In the 2018 FIFA World Cup final, France defeated Croatia 4 goals to 2.  Based on this outcome:
>
>1. How confident should we be that France is the better team?
>
>2. If the same teams played again, what is the chance Croatia would win?

I started with the assumption that for any team against any other team there is some unknown goal-scoring rate, λ.

And I showed that if we know λ, we can compute the probability of scoring $k$ goals in a game:

$f(k; λ) = λ^k \exp(-λ) ~/~ k!$

This function is the [Poisson distribution](https://en.wikipedia.org/wiki/Poisson_distribution), and we can use SciPy to compute it.

For example, if we know that λ is 1.4, we can compute the distribution of $k$ like this:

from scipy.stats import poisson

λ = 1.4
xs = np.arange(11)
ys = poisson.pmf(xs, λ)
pmf_goals = pd.Series(ys, index=xs)

pmf_goals

In theory, the values of $k$ go to infinity, but I cut it off at 10 because higher values have very low probability.

Here's what the distribution of $k$ looks like:

pmf_goals.plot.bar(color='C0', alpha=0.5, label='Poisson distribution')

plt.xlabel('Number of goals')
plt.ylabel('Probability')
plt.title('Distribution of goals scored')
plt.legend();

Because the values of $k$ are discrete integers, I use a bar chart to plot the distribution.

Next I used a gamma distributon to represent the prior distribution of λ.  I set the parameter of the gamma distribution, α, to 1.4, which is the average number of goals each team scores in World Cup play.

I broke the range of values for λ into 101 discrete possibilities and used SciPy to compute the prior probabilities:

from scipy.stats import gamma

α = 1.4
qs = np.linspace(0, 10, 101)
ps = gamma.pdf(qs, α)

prior = pd.Series(ps, index=qs)
prior /= prior.sum()

Here's what the prior distribution looks like:

prior.plot(label='prior', color='gray')

plt.xlabel('Goal scoring rate')
plt.ylabel('Probability')
plt.title('Prior distribution')
plt.legend();

Because the values of $λ$ are continuous, I use a line to plot the distribution.

Next we did a Bayesian update, using the Poisson distribution to compute the likelihood of the data, $k$, for each possible goal scoring rate, $λ$

$ f(k; λ) = λ^k \exp(-λ) ~/~ k! $

Since France scored 4 goals, the likelihood of the data is:

λs = prior.index
k = 4
likelihood = poisson.pmf(k, λs)

And we can use the following function to do the update:

def bayes_update(pmf, likelihood):
    """Do a Bayesian update.

    pmf: Series that represents the prior
    likelihood: sequence of likelihoods

    returns: float probability of the data
    """
    pmf *= likelihood
    prob_data = pmf.sum()
    pmf /= prob_data
    return prob_data

france = prior.copy()
bayes_update(france, likelihood)

And we can do the same thing for Croatia, given that they scored 2 goals:

k = 2
λs = prior.index
likelihood = poisson.pmf(k, λs)

croatia = prior.copy()
bayes_update(croatia, likelihood)

Here's what the results look like.

prior.plot(label='prior', color='gray')
france.plot(label='France', color='C0')
croatia.plot(label='Croatia', color='C3')

plt.xlabel('Goal scoring rate')
plt.ylabel('Probability')
plt.title('Posterior distributions for France and Croatia')
plt.legend();

In the previous notebook we used the following function to compute the mean of a PMF.

def pmf_mean(pmf):
    """Compute the mean of a PMF.

    pmf: Series representing a PMF

    return: float
    """
    return np.sum(pmf.index * pmf)

We can use it to compute the posterior means for France and Croatia.

pmf_mean(france), pmf_mean(croatia)

Based on the outcome of the game, we have some reason to think France is the better team.

But if we look at the posterior distribution of λ for France and Croatia, there is still a lot of overlap, which means we are still not certain which is the better team.

In the next section, we'll use the posterior distributions to compute the "probability of superiority".

### Probability of superiority

Now that we have a posterior distribution for each team, we can answer the first question: How confident should we be that France is the better team?

In the model, "better" means having a higher goal-scoring rate against the opponent.  We can use the posterior distributions to compute the probability that a random value drawn from France's distribution exceeds a value drawn from Croatia's.

The following function takes a Series that represents a PMF and draws a sample from it.

def pmf_choice(pmf, n):
    """Draw a random sample from a PMF.

    pmf: Series representing a PMF
    n: number of values to draw

    returns: NumPy array
    """
    return np.random.choice(pmf.index,
                            size=n,
                            replace=True,
                            p=pmf)

`pmf_choice` uses `np.random.choice`, which chooses `n` values from the PMF with replacement, so the same value can appear more than once.  It uses the probabilities from the PMF as weights, so the number of times each value appears is proportional to its probability.

Here's a sample from the posterior distribution for France.

sample_france = pmf_choice(france, 1000)
sample_france.mean()

And here's a sample for Croatia.

sample_croatia = pmf_choice(croatia, 1000)
sample_croatia.mean()

To estimate the probability of superiority, we can count the number of times the value from France's distribution exceeds the value from Croatia's distribution.

np.mean(sample_france > sample_croatia)

On the basis of one game, we have moderate confidence that France is actually the better team.

### Predicting the rematch

Now we can take on the second question: If the same teams played again, what is the chance Croatia would win?

To answer this question, we'll generate a sample from the "posterior predictive distribution", which is the number of goals we expect a team to score.

If we knew the goal scoring rate, λ, the distribution of goals would be a Poisson distributon with parameter λ.

Since we don't know λ, we can use the sample we generated in the previous section to generate a sample of goals, like this:

goals_france = np.random.poisson(sample_france)

`np.random.poisson` uses each element of `sample_france` to generate a random value; that is, each value in the result is based on a different value of λ.

To see what the resulting distribution looks like, we'll use this function from a previous notebook:

def pmf_from_seq(seq):
    """Make a PMF from a sequence of values.

    seq: sequence

    returns: Series representing a PMF
    """
    pmf = pd.Series(seq).value_counts(sort=False).sort_index()
    pmf /= pmf.sum()
    return pmf

pmf_france = pmf_from_seq(goals_france)
pmf_france.plot.bar(color='C0', label='France')

plt.xlabel('Goals scored')
plt.ylabel('Probability')
plt.title('Predictive distribution')
plt.legend()

goals_france.mean()

This distribution represents two sources of uncertainty: we don't know the actual value of λ, and even if we did, we would not know the number of goals in the next game.

**Exercise:** Generate and plot the predictive distribution for Croatia.

# Solution goes here

In a sense, these distributions represent the outcomes of 1000 simulated games.

**Exercise:** Compute the fraction of simulated rematches Croatia would win, how many France would win, and how many would end in a tie.

# Solution goes here

# Solution goes here

# Solution goes here

Assuming that Croatia wins half of the ties, their chance of winning the rematch is about 33%.

## Summary

In this notebook, we finished off the World Cup problem:

* We used posterior distributions to generate samples of goal-scoring rates.

* We compared samples to compute a "probability of superiority".

* We used samples and `np.random.poisson` to generate samples of goals score and to estimate their distributions.

* We used those distributions to compute the probabilities of winning, losing, and tying in a rematch.

The goal distributions we computed are called "[posterior predictive distributions](https://en.wikipedia.org/wiki/Posterior_predictive_distribution)" because they use posterior distribution to make predictions.

[In the next notebook](https://colab.research.google.com/github/AllenDowney/BiteSizeBayes/blob/master/10_joint.ipynb) we'll take a break from Bayesian methods and learn about an important idea in probability: joint distributions.

But first, here's an exercise where you can practice what you learned in this notebook.

## Exercise

**Exercise:** Here's a variation on the World Cup Problem:

>In the 2014 FIFA World Cup, Germany played Brazil in a semifinal match. Germany scored after 11 minutes and again at the 23 minute mark. At that point in the match, how many goals would you expect Germany to score after 90 minutes? What was the probability that they would score 5 more goals (as, in fact, they did)?

In this version, notice that the data is not the number of goals in a fixed period of time, but the time between goals.

To compute the likelihood of data like this, we can take advantage of the theory of [Poisson processes](https://en.wikipedia.org/wiki/Poisson_point_process).  In our model of a soccer game, we assume that each team has a goal-scoring rate, λ, in goals per game.  And we assume that λ is constant, so the chance of scoring a goal in the same at any moment of the game.

Under these assumptions, the distribution of goals follows a Poisson distribution, as we've already seen.  Also, the time between goals follows an [exponential distribution](https://en.wikipedia.org/wiki/Exponential_distribution).

If the goal-scoring rate is λ, the probability of seeing an interval between goals of $t$ is proportional to the PDF of the exponential distribution:

$f(t; λ) = λ~\exp(-λ t)$

Because $t$ is a continuous quantity, the value of this expression is not really a probability; technically it is a [probability density](https://en.wikipedia.org/wiki/Probability_density_function).  However, it is proportional to the probability of the data, so we can use it as a likelihood in a Bayesian update.

To see what the exponential distribution looks like, let's assume again that λ is 1.4; we can compute the distribution of $t$ like this:

def expo_pdf(t, λ):
    """Compute the PDF of the exponential distribution.

    t: time
    λ: rate

    returns: probability density
    """
    return λ * np.exp(-λ * t)

λ = 1.4
ts = np.linspace(0, 4, 101)
ys = expo_pdf(ts, λ)
pmf_time = pd.Series(ys, index=ts)
pmf_time /= pmf_time.sum()

pmf_time.plot(label='λ = 1.4')

plt.xlabel('Time between goals (games)')
plt.ylabel('Probability')
plt.title('Distribution of time between goals')
plt.legend();

It is counterintuitive, but true, that the most likely time to score a goal is immediately.  After that, the probability of each possible interval is a little lower.

With a goal-scoring rate of 1.4, it is possible that a team will take more than one game to score a goal, but it is unlikely that they will take more than two games.

Now we're ready to solve the problem.  Here are the steps I recommend:

1. Starting with the same gamma prior we used in the previous problem, compute the likelihood of scoring a goal after 11 minutes for each possible value of λ.  Don't forget to convert all times into units of games.

2. Use `bayes_update` to compute the posterior distribution of λ for Germany after the first goal.  If the total probability of the data is greater than 1, don't panic; because the likelihoods are not probabilities, the return value from `bayes_update` is not a probability either.  But the posterior distribution is still valid.

3. Compute the likelihood of scoring another goal after 12 more minutes and do another update.  Plot the prior, posterior after one goal, and posterior after two goals.

4. Use `pmf_choice` to generate a sample of 1000 values from the posterior distribution of goal scoring rate.

5. Use `np.random.poisson` to compute a sample of goals Germany might score during the remaining time in the game, `90-23` minutes.  Note: you will have to think about how to generate predicted goals for a fraction of a game.

6. Compute and plot the PMF of possible goals scored and select from it the probability of scoring 5 more goals.

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here

# Solution goes here


import numpy as np

def griewank(X):
    """ minimum @ 0
    """

    return (
        1
        + (X ** 2).sum(axis=1) / 4000
        - np.prod(
            np.cos(
                X / np.sqrt(np.arange(1, X.shape[-1] + 1))
            ),
            axis=1
        )
    )

# def rastrigin(v):  # min @ 0*
#     return 10*v.size + (v**2 - 10*np.cos(2*np.pi*v)).sum(1)

# def schwefel(v):  # min @ 0*
#     return 418.9829*v.size - (v*np.sin(np.sqrt(np.abs(v)))).sum(1)

def rastrigin(P):
    n = P.shape[-1]
    return (
        P**2 - 10 * np.cos(2*np.pi * P)
    ).sum(axis=1) + 10*n

def schwefel(P):
    n = P.shape[-1]
    return np.sum(
        P * np.sin(
            np.sqrt(
                np.abs(P)
            )
        ),
        axis=1
    ) + 418.9829*n

def sphere(P):
    return np.sum(P**2, axis=1)

def dp(P):
    return (P[:, 0]-1) ** 2 + np.sum(
        (
            2 * P[:, 1:]**2 - P[:, :-1]**2
        ) ** 2 *
        np.arange(2, P.shape[-1] + 1), axis=1
    )

tests = [
    {
        'population_evaluation': griewank,
        'individual_size': 80,
        'domain': (-500, 500)
    },
    {
        'population_evaluation': rastrigin,
        'individual_size': 60,
        'domain': (-5.12, 5.12)
    },
    {
        'population_evaluation': schwefel,
        'individual_size': 60,
        'domain': (-500, 500)
    },
    {
        'population_evaluation': dp,
        'individual_size': 500,
        'domain': (-10, 10)
    },
    {
        'population_evaluation': sphere,
        'individual_size': 60,
        'domain': (-5.12, 5.12)
    }
]

if __name__ == "__main__":
    # pop = np.array([
    #     [1, 2, 3, 0, 1, 0],
    #     [5, 6, 6, 1, 0, 0],
    #     [4, 4, 4, 0, 0, 1]
    # ], dtype=np.float64)
    # x = pop - np.arange(1, pop.shape[-1] + 1)  # [np.newaxis, :]
    # print(x)
    a = np.array([1, -2, -4, 6])
    print(a / a.sum())
    pop = np.vstack((
        np.zeros((1, 15)),
        np.ones((1, 15)) * 600,
        np.arange(15)[np.newaxis, :]
    ))
    print(Griewank().function(pop))


#!/usr/bin/env python3

"""
    Title:  ML Workshop: Simple Linear Regression (SLR)
    Date:   May 15, 2021
    Author: Mario Raciti
"""

import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_validate

np.random.seed(0)

# Create a dataset
# Vars
obs_n = 200
a, b = 3.5, 8
fluctuations = 1
# Feature
X = 1 + 2 * np.random.random(size=(obs_n, 1))
# Target
Y = b + a * X.squeeze() + fluctuations * np.random.randn(obs_n)
# Plot dataset to chart
plt.plot(X, Y, "+")
plt.show()

# Create an SLR model
slr_model = LinearRegression()
# Training
slr_model.fit(X, Y)
print(slr_model.coef_)
print(slr_model.intercept_)

# Predictions
# Single-point prediction
X_pred = np.array([2, ])[:, np.newaxis]
# Multiple-point prediction
X_pred = np.array([2, 3])[:, np.newaxis]
# Multiple-point prediction (dynamic)
first, last, new_obs = X.min(), X.max(), 100
X_pred = np.linspace(first, last, new_obs)[:, np.newaxis]

# Predict
Y_pred = slr_model.predict(X=X_pred)

# Plot predictions to chart
plt.plot(X, Y, '+')
plt.plot(X_pred, Y_pred, 'r-')
plt.show()

# Model Evaluation
# K-fold Cross Validation
K = 5
print(f'Number of folds = {K}\n')
error = 'neg_mean_squared_error'    # NOTE: MSE = RSS/N
results = cross_validate(slr_model, X, Y, cv=K, scoring=error)

# Error for each fold and average error over the folds:
errors = -results['test_score']
print(f'Error for each fold: {errors}')
print(f'Error averaged on all folds: {errors.mean()}')


import numpy as np
import scipy.io as sio
import tensorflow as tf

def one_hot_vector(i):
    one_hot = # Construct a 1D matrix whose cells are filled by zero
    one_hot[i] = # Reassign the position i of one_hot to one
    return one_hot

'''
Step 0: Load the input data, which is stored in digits.mat file.
Then, initialize some variables namely number_of_samples, number_of_features, and number_of_labels
'''

data = sio.loadmat("data/digits.mat")

feature_matrix = data['X']  # the feature matrix is labeled with 'X' inside the file
target_vector = np.squeeze(data['y'])  # the target variable vector is labeled with 'y' inside the file

number_of_samples = feature_matrix.shape[0]  # i.e. 5000 samples
number_of_features = feature_matrix.shape[1]  # i.e. 400 features (20 * 20)
number_of_labels = np.max(target_vector)  # i.e. 1, 2, 3, ..., 10

target_vector_one_hot = np.zeros([number_of_samples, number_of_labels])
for i in range(number_of_samples):
    target_vector_one_hot[i, :] = one_hot_vector(target_vector[i] % 10)

'''
Step 1: Construct TF graph
'''

X = tf.placeholder(tf.float32, [None, number_of_features], name='X')
Y = tf.placeholder(tf.float32, [None, 10], name='Y')  # 0-9 digits recognition. Hence, 10 classes !

thetas = tf.Variable(tf.zeros([number_of_features, 10]), name='Thetas')
theta0 = tf.Variable(tf.zeros([10]), name='theta0')

'''
Step 2: Define the hypothesis function i.e. Softmax function (see Slide# )
'''

hypothesis_function = # Put your code here

'''
Step 3: Define the cost function i.e. the cross-entropy loss
'''

cost_function = # Put your code here

'''
Step 4: Use gradient descent with learning rate of 0.0003 to minimize the cost function
'''

optimizer = # Put your code here

'''
Step 5: Now, we prepare examples for visualizing our test results
'''

# We sample 10 images from the training dataset
random_sample_index = np.random.choice(feature_matrix.shape[0], 10)
feature_random_sample = feature_matrix[random_sample_index, :]
actual_random_sample = target_vector[random_sample_index].T

# Display each randomized image
plt.imshow(feature_random_sample.reshape(-1, 20).T)
plt.axis('off')

class_probability_sample = np.zeros(shape=[10, number_of_labels])

'''
Step 6: Train the model
'''

with tf.Session() as session:
    # Put your code here to:
    # 1) train the model
    # 2) compute the cost for each training step
    # 3) report the number of correct prediction and its accuracy
    # Noted that there may be more than one line of code for this step !

    # Class probability of 10 sampled images
    class_probability_sample = session.run(hypothesis_function, feed_dict={X: feature_random_sample})

    # Compare the actual digits against predicted digits
    print("Actual digit:\n {}".format(actual_random_sample % 10))
    print("Predicted digit:\n {}".format(class_probability_sample.argmax(axis=1)))


import numpy as np
import numpy.linalg as lin
from typing import List, Dict, Any

def multivariate_gaussian(x: np.ndarray, m: np.ndarray, P: np.ndarray) -> float:
    """
    Multivatiate Gaussian Distribution

    :param x: vector
    :param m: distribution mean vector
    :param P: Covariance matrix
    :return: probability density function at x
    """
    first_part = 1 / (((2 * np.pi) ** (x.size / 2.0)) * (lin.det(P) ** 0.5))
    second_part = -0.5 * (x - m) @ lin.inv(P) @ (x - m)
    return first_part * np.exp(second_part)

def multivariate_gaussian_predefined_det_and_inv(x: np.ndarray, m: np.ndarray, detP: np.float64,
                                                 invP: np.ndarray) -> float:
    """
    Multivariate Gaussian Distribution with provided determinant and inverse of the Gaussian mixture.
    Useful in case when we already have precalculted determinant and inverse of the covariance matrix.
    :param x: vector
    :param m: distribution mean
    :param detP: determinant of the covariance matrix
    :param invP: inverse of the covariance matrix
    :return: probability density function at x
    """
    first_part = 1 / (((2 * np.pi) ** (x.size / 2.0)) * (detP ** 0.5))
    second_part = -0.5 * (x - m) @ invP @ (x - m)
    return first_part * np.exp(second_part)

def clutter_intensity_function(z: np.ndarray, lc: int, surveillance_region: np.ndarray):
    """
    Clutter intensity function, with the uniform distribution through the surveillance region, pg. 8
    in "Bayesian Multiple Target Filtering Using Random Finite Sets" by Vo, Vo, Clark.
    :param z:
    :param lc: average number of false detections per time step
    :param surveillance_region: np.ndarray of shape (number_dimensions, 2) giving the range(min and max) for each
                                dimension
    """
    if surveillance_region[0][0] <= z[0] <= surveillance_region[0][1] and surveillance_region[1][0] <= z[1] <= \
            surveillance_region[1][1]:
        # example in two dimensions: lc/((xmax - xmin)*(ymax-ymin))
        return lc / ((surveillance_region[0][1] - surveillance_region[0][0]) * (
                surveillance_region[1][1] - surveillance_region[1][0]))
    else:
        return 0.0

class GaussianMixture:
    def __init__(self, w: List[np.float64], m: List[np.ndarray], P: List[np.ndarray]):
        """
        The Gaussian mixture class

        :param w: list of scalar weights
        :param m: list of np.ndarray means
        :param P: list of np.ndarray covariance matrices

        Note that constructor creates detP and invP variables which can be used instead of P list, for covariance matrix
        determinant and inverse. These lists cen be initialized with assign_determinant_and_inverse function, and
        it is useful in case we already have precalculated determinant and inverse earlier.
        """
        self.w = w
        self.m = m
        self.P = P
        self.detP = None
        self.invP = None

    def set_covariance_determinant_and_inverse_list(self, detP: List[np.float64], invP: List[np.ndarray]):
        """
        For each Gaussian component, provide the determinant and the covariance inverse
        :param detP: list of determinants for each Gaussian component in the mixture
        :param invP: list of covariance inverses for each Gaussian component in the mixture
        """
        self.detP = detP
        self.invP = invP

    def mixture_value(self, x: np.ndarray):
        """
        Gaussian Mixture function for the given vector x
        """
        sum = 0
        if self.detP is None:
            for i in range(len(self.w)):
                sum += self.w[i] * multivariate_gaussian(x, self.m[i], self.P[i])
        else:
            for i in range(len(self.w)):
                sum += self.w[i] * multivariate_gaussian_predefined_det_and_inv(x, self.m[i], self.detP[i],
                                                                                self.invP[i])
        return sum

    def mixture_single_component_value(self, x: np.ndarray, i: int) -> float:
        """
        Single Gaussian Mixture component value for the given vector
        :param x: vector
        :param i: index of the component
        :returns: probability density function at x, multiplied with the component weght at the index i
        """
        if self.detP is None:
            return self.w[i] * multivariate_gaussian(x, self.m[i], self.P[i])
        else:
            return self.w[i] * multivariate_gaussian_predefined_det_and_inv(x, self.m[i], self.detP[i], self.invP[i])

    def mixture_component_values_list(self, x: np.ndarray) -> List[float]:
        """
        Sometimes it is useful to have value of each component multiplied with its weight
        :param x: vector
        :return: List[np.float64]:
        List of components values at x, multiplied with their weight.
        """
        val = []
        if self.detP is None:
            for i in range(len(self.w)):
                val.append(self.w[i] * multivariate_gaussian(x, self.m[i], self.P[i]))
        else:
            for i in range(len(self.w)):
                val.append(
                    self.w[i] * multivariate_gaussian_predefined_det_and_inv(x, self.m[i], self.detP[i], self.invP[i]))
        return val

    def copy(self):
        w = self.w.copy()
        m = []
        P = []
        for m1 in self.m:
            m.append(m1.copy())
        for P1 in self.P:
            P.append(P1.copy())
        return GaussianMixture(w, m, P)

def get_matrices_inverses(P_list: List[np.ndarray]) -> List[np.ndarray]:
    inverse_P_list = []
    for P in P_list:
        inverse_P_list.append(lin.inv(P))
    return inverse_P_list

def get_matrices_determinants(P_list: List[np.ndarray]) -> List[float]:
    """
    :param P_list: list of covariance matrices
    :return:
    """
    detP = []
    for P in P_list:
        detP.append(lin.det(P))
    return detP

def thinning_and_displacement(v: GaussianMixture, p, F: np.ndarray, Q: np.ndarray):
    """
    For the given Gaussian mixture v, perform thinning with probability P and displacement with N(x; F @ x_prev, Q)
    See https://ieeexplore.ieee.org/document/7202905 for details
    """
    w = []
    m = []
    P = []
    for weight in v.w:
        w.append(weight * p)
    for mean in v.m:
        m.append(F @ mean)
    for cov_matrix in v.P:
        P.append(Q + F @ cov_matrix @ F.T)
    return GaussianMixture(w, m, P)

class GmphdFilter:
    def __init__(self, model: Dict[str, Any]):
        """
        The Gaussian Mixture Probability Hypothesis Density filter implementation.
        "The Gaussian mixture probability hypothesis density filter" by Vo and Ma.

        https://ieeexplore.ieee.org/document/1710358

        We assume linear transition and measurement model in the
        following form
            x[k] = Fx[k-1] + w[k-1]
            z[k] = Hx[k] + v[k]
        Inputs:

        - model: dictionary which contains the following elements(keys are strings):

               F: state transition matrix

               H: measurement matrix

               Q: process noise covariance matrix(of variable w[k]).

               R: measurement noise covariance matrix(of variable v[k]).

             p_d: probability of target detection

             p_s: probability of target survival

            Spawning model, see pg. 5. of the paper. It's a Gaussian Mixture conditioned on state

             F_spawn:  d_spawn: Q_spawn: w_spawn: lists of ndarray objects with the same length, see pg. 5

            clutt_int_fun: reference to clutter intensity function, gets only one argument, which is the current measure

               T: U: Jmax: Pruning parameters, see pg. 7.

            birth_GM: The Gaussian Mixture of the birth intensity
        """
        # to do: dtype, copy, improve performance
        self.p_s = model['p_s']
        self.F = model['F']
        self.Q = model['Q']
        self.w_spawn = model['w_spawn']
        self.F_spawn = model['F_spawn']
        self.d_spawn = model['d_spawn']
        self.Q_spawn = model['Q_spawn']
        self.birth_GM = model['birth_GM']
        self.p_d = model['p_d']
        self.H = model['H']
        self.R = model['R']
        self.clutter_density_func = model['clutt_int_fun']
        self.T = model['T']
        self.U = model['U']
        self.Jmax = model['Jmax']

    def spawn_mixture(self, v: GaussianMixture) -> GaussianMixture:
        """
        Spawning targets in prediction step
        """
        w = []
        m = []
        P = []
        for i, w_v in enumerate(v.w):
            for j, w_spawn in enumerate(self.w_spawn):
                w.append(w_v * w_spawn)
                m.append(self.F_spawn[j] @ v.m[i] + self.d_spawn[j])
                P.append(self.Q_spawn[j] + self.F_spawn[j] @ v.P[i] @ self.F_spawn[j].T)
        return GaussianMixture(w, m, P)

    def prediction(self, v: GaussianMixture) -> GaussianMixture:
        """
        Prediction step of the GMPHD filter
        Inputs:
        - v: Gaussian mixture of the previous step
        """
        # v_pred = v_s + v_spawn +  v_new_born
        birth_copy = self.birth_GM.copy()
        # targets that survived v_s:
        v_s = thinning_and_displacement(v, self.p_s, self.F, self.Q)
        # spawning targets
        v_spawn = self.spawn_mixture(v)
        # final phd of prediction
        return GaussianMixture(v_s.w + v_spawn.w + birth_copy.w, v_s.m + v_spawn.m + birth_copy.m,
                               v_s.P + v_spawn.P + birth_copy.P)

    def correction(self, v: GaussianMixture, Z: List[np.ndarray]) -> GaussianMixture:
        """
        Correction step of the GMPHD filter
        Inputs:
        - v: Gaussian mixture obtained from the prediction step
        - Z: Measurement set, containing set of observations
        """
        v_residual = thinning_and_displacement(v, self.p_d, self.H, self.R)
        detP = get_matrices_determinants(v_residual.P)
        invP = get_matrices_inverses(v_residual.P)
        v_residual.set_covariance_determinant_and_inverse_list(detP, invP)

        K = []
        P_kk = []
        for i in range(len(v_residual.w)):
            k = v.P[i] @ self.H.T @ invP[i]
            K.append(k)
            P_kk.append(v.P[i] - k @ self.H @ v.P[i])

        v_copy = v.copy()
        w = (np.array(v_copy.w) * (1 - self.p_d)).tolist()
        m = v_copy.m
        P = v_copy.P

        for z in Z:
            values = v_residual.mixture_component_values_list(z)
            normalization_factor = np.sum(values) + self.clutter_density_func(z)
            for i in range(len(v_residual.w)):
                w.append(values[i] / normalization_factor)
                m.append(v.m[i] + K[i] @ (z - v_residual.m[i]))
                P.append(P_kk[i].copy())

        return GaussianMixture(w, m, P)

    def pruning(self, v: GaussianMixture) -> GaussianMixture:
        """
        See https://ieeexplore.ieee.org/document/7202905 for details
        """
        I = (np.array(v.w) > self.T).nonzero()[0]
        w = [v.w[i] for i in I]
        m = [v.m[i] for i in I]
        P = [v.P[i] for i in I]
        v = GaussianMixture(w, m, P)
        I = (np.array(v.w) > self.T).nonzero()[0].tolist()
        invP = get_matrices_inverses(v.P)
        vw = np.array(v.w)
        vm = np.array(v.m)
        w = []
        m = []
        P = []
        while len(I) > 0:
            j = I[0]
            for i in I:
                if vw[i] > vw[j]:
                    j = i
            L = []
            for i in I:
                if (vm[i] - vm[j]) @ invP[i] @ (vm[i] - vm[j]) <= self.U:
                    L.append(i)
            w_new = np.sum(vw[L])
            m_new = np.sum((vw[L] * vm[L].T).T, axis=0) / w_new
            P_new = np.zeros((m_new.shape[0], m_new.shape[0]))
            for i in L:
                P_new += vw[i] * (v.P[i] + np.outer(m_new - vm[i], m_new - vm[i]))
            P_new /= w_new
            w.append(w_new)
            m.append(m_new)
            P.append(P_new)
            I = [i for i in I if i not in L]

        if len(w) > self.Jmax:
            L = np.array(w).argsort()[-self.Jmax:]
            w = [w[i] for i in L]
            m = [m[i] for i in L]
            P = [P[i] for i in L]

        return GaussianMixture(w, m, P)

    def state_estimation(self, v: GaussianMixture) -> List[np.ndarray]:
        X = []
        for i in range(len(v.w)):
            if v.w[i] >= 0.5:
                for j in range(int(np.round(v.w[i]))):
                    X.append(v.m[i])
        return X

    def filter_data(self, Z: List[List[np.ndarray]]) -> List[List[np.ndarray]]:
        """
        Given the list of collections of measurements for each time step, perform filtering and return the
        estimated sets of tracks for each step.

        :param Z: list of observations(measurements) for each time step
        :return X:
        list of estimated track sets for each time step
        """
        X = []
        v = GaussianMixture([], [], [])
        for z in Z:
            v = self.prediction(v)
            v = self.correction(v, z)
            v = self.pruning(v)
            x = self.state_estimation(v)
            X.append(x)
        return X


import numpy as np
import matplotlib.pyplot as plt

# Make a prediction with weights
def predict(row, weights):
    activation = weights[0]
    for i in range(len(row)-1):
        activation += weights[i + 1] * row[i]
    return 1.0 if activation >=0.0 else -1

# Estimate Perceptron weights using stochastic gradient descent
# n_epoch is number of times to iterate over dataset
# l_rate is the amount by which we will modify the weights in each iteration
def train_weights(train, l_rate, n_epoch):
    weights = [1.0 for i in range(len(train[0]))]
    for epoch in range(n_epoch):
        sum_error = 0.0
        for row in train:
            prediction = predict(row, weights)
            error = row[-1] - prediction
            sum_error += error**2
            # Threshold updation
            weights[0] = weights[0] + l_rate * error
            for i in range(len(row)-1):
                weights[i + 1] = weights[i + 1] + l_rate * error * row[i]
            # For plotting
            # Generating points for showing the line
            if weights[2]!=0:
                y = []
                for i in dataset :
                    y.append(-(weights[0] + weights[1]*i[0])/weights[2])
                plt.cla()
                plt.hold(True)
                plt.scatter(data_array[:,0],data_array[:,1],marker="o",c=data_array[:,2])
                plt.plot(data_array[:,0], y)
                plt.hold(False)
                plt.pause(0.3)
        print('epoch=%d, lrate=%.3f, error=%.3f' % (epoch, l_rate, sum_error))
    return weights

# Calculate weights
dataset = [[1, 43, 1], [2, -43, -1], [3, 58, 1], [4, -37, -1], [5, 73, 1], [6, -34, -1], [7, 74, 1], [8, -30, -1], [9, 85, 1], [10, -15, -1], [11, 73, 1], [12, -11, -1], [13, 83, 1], [14, -14, -1], [15, 95, 1], [16, 5, -1], [17, 110, 1], [18, 1, -1], [19, 115, 1], [20, 16, -1], [21, 111, 1], [22, 25, -1], [23, 126, 1], [24, 23, -1], [25, 133, 1]]

data_array = np.array(dataset)
plt.ion()
plt.figure()
plt.scatter(data_array[:,0],data_array[:,1],marker="o",c=data_array[:,2])
l_rate = 0.01
n_epoch = 2
weights = train_weights(dataset, l_rate, n_epoch)

plt.pause(1000)

print("---------Results---------")
print("Final weights:", weights)


import numpy as np

def random_to_polar(x: np.array):
    """
    Converts an array of 2 random numbers between 0 and 1 into a polar coordinate with distance between 0 and 1 and angle
    between 0 and 2 * pi radians
    :param x: an array [x0, x1] of 2 random numbers
    :return: the corresponding polar coordinate [distance, angle]
    """
    magnitude = x[0]
    angle = x[1] * 2 * np.pi
    return np.array([magnitude, angle])

def polar_to_cartesian(x: np.array):
    """
    Converts a polar coordinate into the corresponding 2D cartesian coordinate
    :param x: an array [distance, angle] representing a polar coordinate
    :return: the corresponding 2D cartesian coordinate [x0, x1]
    """
    cartesian = np.array([x[0] * np.cos(x[1]), x[0] * np.sin(x[1])])
    return cartesian