import numpy as np

def lu(a, sequence=False, swap_times: list = {}):
    """
    「高斯消去法」的 LU 分解.

    本函数可以计算「列主元高斯消元法」、「顺序高斯消元法」的 LU 分解，
    通过参数 sequence 控制，默认 sequence=False 使用「列主元高斯消元法」。

    Args:
        a: np_array_like 方阵 (nxn)
        sequence: bool, True 则使用顺序高斯消去法，False 为列主元的高斯消去法
            default: sequence=False
        swap_times: 这是一个**输出**用的变量，只有传入 dict 变量时才有效。
            若使用「列主元高斯消元法」（sequence=False）
            则，置 swap_times['swap_times'] = 行交换次数。
            这个值正常的输出中不需要，但在一些问题，比如，
            利用 LU 分解求行列式时，得到 swap_times 会很有帮助。

    Returns:
        (l, u, p): result

        l: np.array, Lower triangle result (nxn)
        u: np.array, Upper triangle result (nxn)
        p: np.array, Permutation: 交换后的行顺序 (n)
            p = None if sequence=True

    Raises:
        Exception: 存在为零的主元素
    """
    a = np.array(a, dtype=np.float)  # copy

    assert a.shape[0] == a.shape[1]
    n = a.shape[0]

    if not sequence:
        # p 记录行交换的过程，使用「列主元高斯消元法」才使用，否则为 None
        p = np.array([k for k in range(n)])
        # swap_times:  行交换次数
        if isinstance(swap_times, dict):
            swap_times['swap_times'] = 0
    else:
        p = None

    for k in range(n-1):
        if not sequence:
            i_max = k + np.argmax(np.abs(a[k:n, k]))

            if i_max != k:
                a[[i_max, k]] = a[[k, i_max]]  # swap rows
                p[[i_max, k]] = p[[k, i_max]]  # record
                swap_times['swap_times'] += 1

        if a[k][k] == 0:
            raise Exception("存在为零的主元素")

        for i in range(k+1, n):
            a[i][k] /= a[k][k]  # L @ 严格下三角
            for j in range(k+1, n):
                a[i][j] -= a[i][k] * a[k][j]  # U @ 上三角

    # print(a, p)

    # Uncommit the following lines to get a Permutation Matrix
    # Only for sequence=False
    # pm = np.zeros_like(a)
    # for i, v in enumerate(p):
    #     pm[i, v] = 1
    #     print(pm)

    return np.tril(a, k=-1) + np.identity(a.shape[0]), np.triu(a), p

def solve_lu(b, l, u, p=None):
    """用 lu(a) 得到的 `pa=lu` 分解的结果求解原方程组 `ax=b` 的解 x。

    若 p 不为 None 则使用「列主元高斯消元」，p 为 None表示使用「顺序高斯消元」。

        # `@` means matrix multiplication, refer: https://docs.python.org/reference/expressions.html#binary-arithmetic-operations
        b = p @ b if p != None
        l @ y = b
        u @ x = y

    Args:
        b: np_array_like, 原方程组的右端常数（n）
        l: np_array_like, Lower triangle of lu_seq(a)
        u: np_array_like, Upper triangle of lu_seq(a)
        p: np_array_like, LU分解中交换后的行顺序
            default p=None: 未做行交换，即使用顺序高斯消去法

        使用列主元高斯消元法时，l, u, p 使用 lu(a) 得到的结果即可：
            solve_lu(b, *lu(a))
        或者使用顺序高斯消元：
            solve_lu(b, *lu(a, sequence=True))  # p=None

    Returns:
        x : np.array `ax=b` 的解（n）
    """
    assert np.shape(l) == np.shape(u)
    assert np.shape(l)[0] == np.shape(b)[0]

    n = np.shape(l)[0]

    # do swap
    if p is not None:
        b = [b[v] for v in p]

    # L * y = b
    y = np.zeros(n, dtype=np.float)
    y[0] = b[0]
    for i in range(1, n):
        bi = b[i]
        for j in range(0, i):
            bi -= y[j] * l[i][j]
        y[i] = bi / l[i][i]
    # print(y)

    # U * x = y
    x = np.zeros(n, dtype=np.float)
    x[n-1] = y[n-1] / u[n-1][n-1]
    for i in range(n-2, -1, -1):  # from n-2 (included) to 0 (included)
        yi = y[i]
        for j in range(i+1, n):
            yi -= x[j] * u[i][j]
        x[i] = yi / u[i][i]
    # print(x)

    return x

def det(a):
    """矩阵行列式

    利用 LU 分解（列主元高斯消元）求方阵 a 的行列式: d = det(a)

    Args:
        a: np_array_like, 要求行列式的矩阵

    Returns:
        d: float, a 的行列式值。

    Reference:
        https://blog.csdn.net/nstarLDS/article/details/106074256
    """
    swap_times = {}
    l, u, p = lu(a, sequence=False, swap_times=swap_times)
    sign = -1 if swap_times['swap_times'] % 2 == 1 else 1
    return np.prod(np.diag(u)) * sign

def inv(a):
    """矩阵的逆

    利用 LU 分解（列主元高斯消元）求方阵 a 的逆矩阵: x = inv(a)

    Args:
        a: np_array_like, 待求逆的矩阵

    Returns:
        x: float, a 的逆矩阵

    Reference:
        https://en.wikipedia.org/wiki/LU_decomposition#Inverting_a_matrix
    """
    l, u, p = lu(a)

    X = np.zeros_like(a, dtype=np.float)
    B = np.identity(np.shape(a)[0])

    for i, b in enumerate(B.T):  # iter on cols
        x = solve_lu(b, l, u, p)
        X[:, i] = x

    return X


"""
Laborator 5
"""

import numpy as np
import proceduri as proc
import time

# Exercițiul 1
A = np.array([[0., 1., 1.],
              [2., 1., 5.],
              [4., 2., 1.]])
b = np.array([[3.], [5.], [1.]])
tol = 10 ** (-10)
L, U, w = proc.FactLU(A, b, tol)

b_nou = np.copy(b)
for i in range(np.shape(b)[0]):
    b_nou[i] = b[w[i]]

y = proc.metSubAsc(L, b_nou, tol)
x = proc.metSubDesc(U, y, tol)
print(f'Soluția Sistemului: \n{x}')
# print(A@x)   # înmulțire de matrici (verificare dacă e egal cu b)

# Exercițiul 2
n = 100
tol = 10 ** (-10)
A = np.random.rand(n, n) * 10     # A -> matrice numere random
b = np.zeros((n, 1))              # b -> tb calculat după formulă cerință
for i in range(n):
    sum = 0
    for j in range(n):
        sum += A[i][j]
    b[i] = sum
# print(f'Matricea A: \n{A}')
# print(f'Matricea b: \n{b}')

# a) b)
L, U, w = proc.FactLU(A, b, tol)
b_nou = np.copy(b)
for i in range(np.shape(b)[0]):
    b_nou[i] = b[w[i]]
y = proc.metSubAsc(L, b_nou, tol)
x = proc.metSubDesc(U, y, tol)
# print(f'Soluție Sistem: \n{x}')

# c) d)
x_old = np.copy(x)
tic = time.time()
L, U, w = proc.FactLU(A, b, tol)
b_nou = np.zeros((n, 1))

for k in range(0, 100):
    b_nou = x_old[:] + 2
    for i in range(np.shape(b)[0]):
        b_nou[i] = b[w[i]]

    y = proc.metSubAsc(L, b_nou, tol)
    x_new = proc.metSubDesc(U, y, tol)

    x_old = x_new[:]

toc = time.time() - tic
#print("Sol la ultima iteratie\n",x_old)
print("In timp de", toc)

tic = time.time()
x_old = x
for k in range(0, 100):
    x_new = proc.GaussPP(A, x_old + 2, tol)
    x_old = x_new[:]
toc = time.time() - tic
#print("Sol la ultima iteratie\n",x_old)
print("In timp de", toc)

Calcul Numeric (CN)/Laborator/Laborator 5/lab5.py


"""
General purpose math functions, mostly geometric in nature.
"""

import numpy as np

def cart2pol(x, y):
    """
    Convert Cartesian to Polar Coordinates. All input arguments must be the same shape.
    :param x: x-coordinate in Cartesian space
    :param y: y-coordinate in Cartesian space
    :return: A 2-tuple of values:
        theta: angular coordinate/azimuth
        r: radial distance from origin
    """
    return np.arctan2(y, x), np.hypot(x, y)

def cart2sph(x, y, z):
    """
    Transform cartesian coordinates to spherical. All input arguments must be the same shape.

    :param x: X-values of input co-ordinates.
    :param y: Y-values of input co-ordinates.
    :param z: Z-values of input co-ordinates.
    :return: A 3-tuple of values, all of the same shape as the inputs.
        (<azimuth>, <elevation>, <radius>)
    azimuth and elevation are returned in radians.

    This function is equivalent to MATLAB's cart2sph function.
    """
    hxy = np.hypot(x, y)
    r = np.hypot(hxy, z)
    el = np.arctan2(z, hxy)
    az = np.arctan2(y, x)
    return az, el, r

def grid_2d(n):
    grid_1d = np.ceil(np.arange(-n/2, n/2)) / (n/2)
    x, y = np.meshgrid(grid_1d, grid_1d, indexing='ij')
    phi, r = cart2pol(x, y)

    return {
        'x': x,
        'y': y,
        'phi': phi,
        'r': r
    }

def grid_3d(n):
    grid_1d = np.ceil(np.arange(-n/2, n/2)) / (n/2)
    x, y, z = np.meshgrid(grid_1d, grid_1d, grid_1d, indexing='ij')
    phi, theta, r = cart2sph(x, y, z)

    # TODO: Should this theta adjustment be moved inside cart2sph?
    theta = np.pi/2 - theta

    return {
        'x': x,
        'y': y,
        'z': z,
        'phi': phi,
        'theta': theta,
        'r': r
    }

def angles_to_rots(angles):
    n_angles = angles.shape[-1]
    rots = np.zeros(shape=(3, 3, n_angles))

    for i in range(n_angles):
        rots[:, :, i] = erot(angles[:, i])
    return rots

def erot(angles):
    return zrot(angles[0]) @ yrot(angles[1]) @ zrot(angles[2])

def zrot(theta):
    sin, cos = np.sin(theta), np.cos(theta)
    return np.array([[cos, -sin, 0], [sin, cos, 0], [0, 0, 1]])

def yrot(theta):
    sin, cos = np.sin(theta), np.cos(theta)
    return np.array([[cos, 0, sin], [0, 1, 0], [-sin, 0, cos]])


# coding: utf-8
import numpy as np
from time import time
import sys

def find_primes(maxN):
    x1 = np.arange(maxN + 1, dtype=np.int64)
    b1 = np.zeros(np.shape(x1), dtype=np.bool)
    b1[x1 > 1] = True
    maxN2 = np.int64(maxN**(0.5) + 1)
    for n in range(2, maxN2 + 1):
        b1[2*n::n] = False
    return x1[b1]

def prime_factors(N):
    pNums = find_primes(N//2 + 1)
    pExps = np.zeros(np.shape(pNums), dtype=int)
    max_exp = int(np.log(N)/np.log(2))
    for n in range(1, max_exp + 1):
        pExps[np.mod(N, pNums**n) == 0] = n
    pN = pNums[pExps > 0]
    pE = pExps[pExps > 0]
    if 0 < np.size(pN) < 10:
        disp_pf(N, pN, pE)
    elif np.size(pN) == 0:
        print('{N} is a prime number!'.format(N=N))
    else:
        pass
    return pN, pE

def find_lcm(num_array):
    Nmax = max(num_array)
    pNums = find_primes(Nmax + 1)
    pExps = np.zeros(np.shape(pNums), dtype=int)
    for N in num_array:
        pExps2 = np.zeros(np.shape(pNums), dtype=int)
        if N in pNums:
            pExps2[pNums == N] = 1
        else:
            max_exp = int(np.log(N)/np.log(2))
            for n in range(1, max_exp + 1):
                pExps2[np.mod(N, pNums**n) == 0] = n
        pExps = np.maximum(pExps, pExps2)
    pN = pNums[pExps > 0]
    pE = pExps[pExps > 0]
    outN = np.product(pN**pE)
    if 0 < np.size(pN) < 10:
        disp_pf(outN, pN, pE)
    else:
        pass
    return outN, pN, pE

def disp_pf(N, pNums, pExps):
    factors1 = []
    for n, e in zip(pNums, pExps):
        if e > 1:
            factor = '{n:,d}^{e}'.format(n=n, e=e)
        else:
            factor = '{n:,d}'.format(n=n)
        factors1.append(factor)
    print('\n{N:,d} = '.format(N=N) + ' * '.join(factors1))

def test_fun1(upper_limit):
    t0 = time()
    prime_array = find_primes(upper_limit)
    t1 = time()
    Nprime = np.size(prime_array)
    print('\nFound {0:,d} prime numbers in {1:.4e} sec'.format(Nprime, t1 - t0))
    print('\nOr, ~{0:,d} prime numbers per second'.format(int(Nprime/(t1 - t0))))
    print('\nPython version: {0}'.format(sys.version))
    return prime_array

if __name__ == "__main__":
    out1 = test_fun1(np.int64(1e+8))
#    num_array = list(range(2, 11))
#    lcm, pN, pE = find_lcm(num_array)


from numpy.linalg import eig, inv, norm, cond
import numpy as np
from tabulate import tabulate

myA=[
 [3.278164, 1.046583, -1.378574],
 [1.046583, 2.975937, 0.934251],
 [-1.378574, 0.934251, 4.836173]
]

myB = [ [-0.527466],
 [2.526877],
 [5.165441]]

def toFixed(numObj, digits=0):
    return f"{numObj:.{digits}f}"

# --- end of исходные данные

# 1)
# --- вывод системы на экран
def FancyPrint(A, B, selected):
    for row in range(len(B)):
        print("(", end='')
        for col in range(len(A[row])):
             print("\t{1:10.2f}{0}".format(" " if (selected is None
or selected != (row, col)) else "*", A[row][col]), end='')
        print("\t) * (\tX{0}) = (\t{1:10.2f})".format(row + 1, B[row][0]))
# --- end of вывод системы на экран

# --- перемена местами двух строк системы
def SwapRows(A, B, row1, row2):
    A[row1], A[row2] = A[row2], A[row1]
    B[row1], B[row2] = B[row2], B[row1]
# --- end of перемена местами двух строк системы

# --- деление строки системы на число
def DivideRow(A, B, row, divider):
    A[row] = [a / divider for a in A[row]]
    B[row][0] /= divider
# --- end of деление строки системы на число

# --- сложение строки системы с другой строкой, умноженной на число
def CombineRows(A, B, row, source_row, weight):
    A[row] = [(a + k * weight) for a, k in zip(A[row], A[source_row])]
    B[row][0] += B[source_row][0] * weight
# --- end of сложение строки системы с другой строкой, умноженной начисло

# --- решение системы методом Гаусса (приведением к треугольному виду)
def Gauss(A, B):
    column = 0
    while (column < len(B)):
        current_row = None
        for r in range(column, len(A)):
            if current_row is None or abs(A[r][column]) > abs(A[current_row][column]):
                 current_row = r
        if current_row is None:
            return None
        if current_row != column:
            SwapRows(A, B, current_row, column)
        DivideRow(A, B, column, A[column][column])
        for r in range(column + 1, len(A)):
            CombineRows(A, B, r, column, -A[r][column])
        column += 1
    X = [0 for b in B]
    for i in range(len(B) - 1, -1, -1):
        X[i] = B[i][0] - sum(x * a for x, a in zip(X[(i + 1):], A[i][(i + 1):]))
    return X
# --- end of решение системы методом Гаусса (приведением к треугольному виду)

def printVector(X):
    print("\n".join("X{0} =\t{1:10.2f}".format(i + 1, x) for i, x in enumerate(X)))

# 1)
print("Исходная система:")
FancyPrint(myA, myB, None)
print("Решаем методом Гаусса:")
xGauss = Gauss(np.copy(myA), np.copy(myB))
printVector(xGauss)
print("-------------------------------------------------")

# 2)
def norminf (H):
    return np.amax(H)

# 3)
def infprecision(k, H, g):
    f = norminf(H)
    return (f ** k) * norminf(np.zeros((3, 1))) + f ** k * norminf(g) / (1 - f)

# 4)
def frobnormvector(X):
    sum = 0.0
    for i in range(len(X)):
        c = X[i]
        sum += (abs(X[i])) ** 2
    return sum ** 0.5

def frobnorm (A):
    sum = 0.0
    for i in range(len(A)):
        for j in range(len(A[0])):
            sum += (abs(A[i][j])) ** 2
    return sum ** 0.5

def aprprecision (k, H, g):
    f = frobnorm(H)
    return (f ** k) * frobnormvector(np.zeros((3, 1))) + f ** k * frobnormvector(g) / (1 - f)

def apostprecision (H, xk, xkplus1):
    return frobnorm(H) * frobnorm(xk - xkplus1) / (1 - frobnorm(H))

def lusthernik (H, xk, xkplus1):
    x_ = xk + (xkplus1 - xk) / (1 - max(abs(np.linalg.eigvals(H))))
    return x_

def makezero():
    return np.zeros((3,1))

def factprecision(x):
    b = np.copy(xGauss)
    for i in range(3):
        k = b[i] - x[i][0]
        b[i] = k
    prec = frobnormvector(b)
    #prec = np.linalg.norm(b)
    print("Фактическая погрешность: ", prec)
    return prec

def simpleiter (k, H, g):
    xk = makezero()
    xklust = makezero()
    for i in range (k):
        xkplus1 = H @ xk + g
        xklust = np.copy(xk)
        xk = np.copy(xkplus1)
    print ("Решение: ")
    print(xkplus1)
    fiter = factprecision(xkplus1)
    print ("Априорная оценка: ", aprprecision(k, H, g))
    print("Апостериорная оценка: ", apostprecision(H, xk, xkplus1))
    l = lusthernik(H, xklust, xkplus1)
    print("Решение, уточнённое по Люстернику: ")
    print(l)
    factprecision(l)
    return xkplus1, fiter

# 5)
def zeidel (k, H, g):
    xk = makezero()
    Hl = np.tril(H, k=-1)
    Hr = np.triu(H, k=0)
    E = np.eye(3, 3, dtype=np.double)
    Hseid = np.linalg.inv(E - Hl) @ Hr
    gSeid = np.linalg.inv(E - Hl) @ g
    for i in range (k):
        xkplus1 = Hseid @ xk + gSeid
        xk = np.copy(xkplus1)
    return xkplus1

def radius (H):
    Hl = np.tril(H, k=-1)
    Hr = np.triu(H, k=0)
    E = np.eye(3, 3, dtype=np.double)
    x = np.linalg.inv(E - Hl) @ Hr
    return spectradius(x)

def spectradius(H):
    return np.max(np.abs(np.linalg.eigvals(H)))

# 7)
def upperrelax(k, H, g):
    p = spectradius(H)
    q = 2 / (1 + np.sqrt(1 - p * p))
    xkplus1 = xk = makezero()
    for m in range(k):
        for i in range(3):
            sum = sum1 = 0
            for j in range(i - 1):
                sum += H[i][j] * xkplus1[j]
            for j in range(i + 1, 3):
                sum1 += H[i][j] * xk[j]
            xkplus1[i] = xk[i] + q * (sum + sum1 - xk[i] + g[i])
    return xkplus1

# 2)
E = np.eye(3, 3, dtype=np.double)
Hd = E - inv(np.diag(np.diag(myA))) @ myA
gD = inv(np.diag(np.diag(myA))) @ myB
print("Норма H: ", norminf(Hd))
print("-------------------------------------------------")

# 3)
print("Априорная погрешность для х(7): ", infprecision(7, Hd, gD))
print("-------------------------------------------------")

# 4)
print("Метод простой итерации: ")
xsimp, fiter = simpleiter(7, Hd, gD)
print("-------------------------------------------------")

# 5)
print("Метод Зейделя: ")
xseid = zeidel(7, Hd, gD)
print(xseid)
fseid = factprecision(xseid)

print("Сравним с решением, полученным методом простой итерации:")
print(xsimp - xseid)
print("-------------------------------------------------")

# 6)
print("Радиус: ", radius(Hd))
print("-------------------------------------------------")

# 7)
print("Метод верхней релаксации: ")
uppx = upperrelax(7, Hd, gD)
print(uppx)
fuppx = factprecision(uppx)
print("-------------------------------------------------")

# 8)
Cond = []
Cond.append([0.0, fiter, fseid, fuppx])

print(tabulate(Cond, headers=['Gauss', 'Simple iteration', 'Seidel', 'Successive over-relaxation'],
               tablefmt='pipe', numalign="right"))


# for decimal division
from __future__ import division

import cvxopt
import numpy as np
from pylab import *
import math

from cvxpy import *

# Taken from CVX website http://cvxr.com/cvx/examples/
# Example: Section 5.2.5: Mixed strategies for matrix games (LP formulation)
# Ported from cvx matlab to cvxpy by Misrab Faizullah-Khan
# Original comments below

# Boyd & Vandenberghe, "Convex Optimization"
# Joelle Skaf - 08/24/05
#
# Player 1 wishes to choose u to minimize his expected payoff u'Pv, while
# player 2 wishes to choose v to maximize u'Pv, where P is the payoff
# matrix, u and v are the probability distributions of the choices of each
# player (i.e. u>=0, v>=0, sum(u_i)=1, sum(v_i)=1)
# LP formulation:   minimize    t
#                       s.t.    u >=0 , sum(u) = 1, P'*u <= t*1
#                   maximize    t
#                       s.t.    v >=0 , sum(v) = 1, P*v >= t*1

# Input data
n = 12
m = 12
P = cvxopt.normal(n,m)

# Variables for two players
x = Variable(n)
y = Variable(m)
t1 = Variable()
t2 = Variable()

# Note in one case we are maximizing; in the other we are minimizing
objective1 = Minimize(t1)
objective2 = Maximize(t2)

constraints1 = [ x>=0, sum_entries(x)==1, P.T*x <= t1 ]
constraints2 = [ y>=0, sum_entries(y)==1, P*y >= t2 ]

p1 = Problem(objective1, constraints1)
p2 = Problem(objective2, constraints2)

# Optimal strategy for Player 1
print 'Computing the optimal strategy for player 1 ... '
result1 = p1.solve()
print 'Done!'

# Optimal strategy for Player 2
print 'Computing the optimal strategy for player 2 ... '
result2 = p2.solve()
print 'Done!'

# Displaying results
print '------------------------------------------------------------------------'
print 'The optimal strategies for players 1 and 2 are respectively: '
print x.value, y.value
print 'The expected payoffs for player 1 and player 2 respectively are: '
print result1, result2
print 'They are equal as expected!'
## ISSUE: THEY AREN'T EXACTLY EQUAL FOR SOME REASON!


import numpy as np
import scipy as sp
import sympy as sm

def gauss_jordan(A,no_reduction=False):
    """ Gauss-Jordan elimination

    Convert a matrix to its row (reduced) echelon form.

    Args:

        A (ndarray): input matrix to work on in-place
        no_reduction (bool): stop when non-reduced echelon form is reached

    """

    n,_m = A.shape

    # row echelon form (Gauss elimination)
    for i in range(0, n):

        # a. search for maximum in this column
        maxrow = i + np.argmax(abs(A[i:,i]))

        # b. swap maximum row with current row (column by column)
        temp = A[maxrow,i:].copy()
        A[maxrow,i:] = A[i,i:]
        A[i,i:] = temp

        # b. make all rows below this one 0 in current column
        for k in range(i+1, n):
            c = -A[k,i]/A[i,i]
            A[k,i] = 0
            A[k,i+1:] += c*A[i,i+1:]

    if no_reduction:
        return

    # reduced row echelon form (Gauss-Jordan elimination)
    for i in range(n-1,-1,-1):

        # a. normalize this row
        c = A[i,i]
        A[i,:] /= c

        # b. make all rows above this one 0 in the current column
        for j in range(0,i):
            c = A[j,i]
            A[j,:] -= c*A[i,:]

def gauss_seidel_split(A):
    """ split A matrix in additive lower and upper triangular matrices

    Args:

        A (ndarray): input matrix

    Returns:

        L (ndarray): lower triangular matrix
        U (ndarray): upper triangular matrix (zero diagonal)

    """

    L = np.tril(A)
    U = np.triu(A)
    np.fill_diagonal(U,0)
    return L,U

def solve_with_forward_substitution(L,RHS):
    """ solve matrix equation with forward substitution

    Args:

        L (ndarray): lower triangular matrix
        RHS (ndarray): vector of right-hand-side variables

    Returns:

        x (ndarray): Solution vector

    """

    n = RHS.size
    x = np.zeros(n)
    for i in range(n):
        x[i] = RHS[i]
        for j in range(i):
            x[i] -= L[i,j]*x[j]
        x[i] /= L[i,i]

    return x

def solve_with_backward_substitution(U,RHS):
    """ solve matrix equation with backward substitution

    Args:

        L (ndarray): uppper triangular matrix
        RHS (ndarray): vector of right-hand-side variables

    Returns:

        x (ndarray): Solution vector

    """

    n = RHS.size
    x = np.zeros(n)
    for i in reversed(range(n)):
        x[i] = RHS[i]
        for j in range(i+1,n):
            x[i] -= U[i,j]*x[j]
        x[i] /= U[i,i]

    return x

def gauss_seidel(A,b,x0,max_iter=500,tau=10**(-8),do_print=False):
    """ solve matrix equation with Gauss-Seidel

    Args:

        A (ndarray): LHS matrix
        b (ndarray): RHS vector
        x0 (ndarray): guess on solution
        max_iter (int): maximum number of iterations (optional)
        tau (float): tolerance level
        do_print (bool): indicator for whether to print or not

    Returns:

        x (ndarray): Solution vector

    """

    converged = False

    # a. split
    L,U = gauss_seidel_split(A)

    # b. iterate
    x = x0
    i = 0

    if do_print:
        print('  ',x)

    while i < max_iter and not converged:

        # i. save previous
        x_prev = x

        # ii. compute RHS
        y = b-U@x

        # iii. solve with forward substituion
        x = solve_with_forward_substitution(L,y)
        #x = sp.linalg.solve_triangular(L,y,lower=True) # equivalent, but faster

        # iv. check convergence
        max_abs_diff = np.max(np.abs(x-x_prev))
        if max_abs_diff < tau:
            converged = True

        # v. misc
        if do_print:
            print(f'{i:2d}',x)

        i += 1

    return x

def lu_decomposition(A):
    """ compute LU decomposition

    Args:

        A (ndarray): input matrix

    Returns:

        L (ndarray): lower triangular matrix
        U (ndarray): upper triangular matrix

    """

    n = len(A)

    # a. create zero matrices for L and U
    L = np.zeros((n,n))
    U = np.zeros((n,n))

    # b. set diagonal of L to one
    np.fill_diagonal(L,1)

    # c. perform the LU Decomposition
    for j in range(n):

        for i in range(j+1):
            c = U[:,j]@L[i,:]
            U[i][j] = A[i][j] - c

        for i in range(j, n):
            c = U[:j,j]@L[i,:j]
            L[i][j] = (A[i][j] - c) / U[j][j]

    return L,U

def construct_sympy_matrix(positions,name='a'):
    """ construct sympy matrix with non-zero elements in positions

    Args:

        Positions (list): list of positions in strings, e.g. ['11','31']

    Returns:

        mat (sympy.matrix): Sympy Matrix

    """

    # a. dictionary of element with position as key and a_position as value
    entries = {f'{ij}':sm.symbols(f'{name}_{ij}') for ij in positions}

    # b. function for creating element or zero
    add = lambda x: entries[x] if x in entries else 0

    # c. create matrix
    mat_as_list = [[add(f'{1+i}{1+j}') for j in range(3)] for i in range(3)]
    mat = sm.Matrix(mat_as_list)

    return mat

def fill_sympy_matrix(A_sm,A,name='a'):

    n,m = A.shape

    # a. make all substitution
    A_sm_copy = A_sm
    for i in range(n):
        for j in range(m):
            if not A[i,j] == 0:
                A_sm_copy = A_sm_copy.subs(f'{name}_{1+i}{1+j}',A[i,j])

    # b. lambdify with no inputs
    f = sm.lambdify((),A_sm_copy)

    # c. return filled matrix
    return f()


# Question 3, Lab 07
# AB Satyaprakash, 180123062

# imports
import numpy as np

# functions

def F(t, part):
    if part == 'a':
        return np.sqrt(t**2 + 6 + 2*t) - 1
    else:
        return (4+np.cos(2)-np.cos(2*t))/(2*(t**2))

def f(t, y, part):
    if part == 'a':
        return (t+1)/(y+1)
    else:
        return (np.sin(2*t) - 2*t*y)/(t**2)

def ModEuler(t, y, h, part):
    k1 = f(t, y, part)
    k2 = k1 + f(t+h, y+h*k1, part)
    return y + h*k2/2

# program body
# part (a)

h, t, y = 0.5, 1, 2
while t <= 2:
    print('For part (a) y({}) is approximated as {}'.format(t, y))
    print('Actual value of y({}) is given by {}\n'.format(t, F(t, 'a')))
    y = ModEuler(t, y, h, 'a')
    t += h

print('----------------------------------------------------------------')

# part (b)
h, t, y = 0.25, 1, 2
while t <= 2:
    print('For part (b) y({}) is approximated as {}'.format(t, y))
    print('Actual value of y({}) is given by {}\n'.format(t, F(t, 'b')))
    y = ModEuler(t, y, h, 'b')
    t += h

Semester 6/MA 322 (Scientific Computing Theory)/Labs/Lab 7/Code/q3.py


from copy import deepcopy
import numpy as np
import pandas as pd
from sklearn.datasets import make_blobs
from joblib import Parallel, delayed

# Importing the dataset
data = pd.read_csv('xclara.csv')
print("Input Data and Shape")
print(data.shape)
data.head()

# Getting the values and plotting it
f1 = data['V1'].values
f2 = data['V2'].values
X = np.array(list(zip(f1, f2)))

# Euclidean Distance Caculator
def dist(a, b, ax=1):
    return np.linalg.norm(a - b, axis=ax)

def assignment(x, C):

    distances = dist(x, C)
    cluster = np.argmin(distances)
    ci = cluster

    return ci

def centroidcompute(X, clusters, i):

    points = [X[j] for j in range(len(X)) if clusters[j] == i]

    return np.mean(points, axis=0)

# Number of clusters
k = 3

# X coordinates of random centroids
C_x = np.random.randint(0, np.max(X)-20, size=k)
# Y coordinates of random centroids
C_y = np.random.randint(0, np.max(X)-20, size=k)
C = np.array(list(zip(C_x, C_y)), dtype=np.float32)
print("Initial Centroids")
print(C)

# To store the value of centroids when it updates
C_old = np.zeros(C.shape)
# Cluster Lables(0, 1, 2)
clusters = np.zeros(len(X))
# Error func. - Distance between new centroids and old centroids
error = dist(C, C_old, None)
# Loop will run till the error becomes zero
while error != 0:
    # Assigning each value to its closest cluster
    # for i in range(len(X)):
    #     distances = dist(X[i], C)
    #     cluster = np.argmin(distances)
    #     clusters[i] = cluster
    clusters = Parallel(n_jobs=2)(delayed(assignment)(X[i], C) for i in range(len(X)))

    # Storing the old centroid values
    C_old = deepcopy(C)

    # Finding the new centroids by taking the average value
    # for i in range(k):
    #     points = [X[j] for j in range(len(X)) if clusters[j] == i]
    #     C[i] = np.mean(points, axis=0)
    b = Parallel(n_jobs=2)(delayed(centroidcompute)(X, clusters, i) for i in range(k))
    for i in range(k):
        C[i] = b[i]

    error = dist(C, C_old, None)

'''
==========================================================
scikit-learn
==========================================================
'''

from sklearn.cluster import KMeans

# Number of clusters
kmeans = KMeans(n_clusters=3)
# Fitting the input data
kmeans = kmeans.fit(X)
# Getting the cluster labels
labels = kmeans.predict(X)
# Centroid values
centroids = kmeans.cluster_centers_

# Comparing with scikit-learn centroids
print("Final Centroid Values")
print("From scratch done by us:")
print(C) # From Scratch
print("From scikit-learn package:")
print(centroids) # From sci-kit learn


import scipy.stats
import numpy as np

def welch_t_test(samples_x, samples_y, diff: float = 0.0, tail: str = "both"):
    assert tail in {"both", "left", "right"}

    mean_x = np.mean(samples_x)
    mean_y = np.mean(samples_y)

    n_x = len(samples_x)
    n_y = len(samples_y)

    var_x_norm = np.var(samples_x, ddof=1) / n_x
    var_y_norm = np.var(samples_y, ddof=1) / n_y

    pooled_var = var_x_norm + var_y_norm

    test_statistic = (mean_x - mean_y - diff) / np.sqrt(pooled_var)

    df = np.square(pooled_var) / (
        np.square(var_x_norm) / (n_x - 1) + np.square(var_y_norm) / (n_y - 1)
    )
    null_dist = scipy.stats.t(df)

    if tail == "both":
        p_value = 2.0 * null_dist.cdf(-abs(test_statistic))

    elif tail == "left":
        p_value = null_dist.cdf(test_statistic)

    else:
        p_value = null_dist.sf(test_statistic)

    return test_statistic, p_value

def two_sample_t_test_equal_var(
    samples_x, samples_y, diff: float = 0.0, tail: str = "both"
):
    """Two sample t-test for normal data to check if pop. mean differs by `diff`.

    Assumptions:
        i.i.d. x_{1}, ..., x_{n} ~ N(mu_{x}, sigma^{2})
        i.i.d. y_{1}, ..., y_{m} ~ N(mu_{y}, sigma^{2})
        Note 1: that the variance for all distributions are the same, sigma^{2}.
        Note 2: `n` may be different from `m`.

    Test statistic: t = (x_mean - y_mean - diff) / sqrt(pooled_var)
    where:
        pooled_var = ((n - 1) * sample_var_x + (m - 1) * sample_var_y) * norm_factor,
        norm_factor = (1 / n + 1 / m) / (n + m - 2)

    null distribution: T ~ t(n + m - 2), there t is the t-student distribution.

    H0: x_mean - y_mean = `diff`
    HA:
        if tail = `both` : x_mean - y_mean != `diff`
        if tail = `left` : x_mean - y_mean < `diff`
        if tail = `right`: x_mean - y_mean > `diff`
    """
    assert tail in {"both", "left", "right"}

    mean_x = np.mean(samples_x)
    mean_y = np.mean(samples_y)

    n_x = len(samples_x)
    n_y = len(samples_y)

    var_x = np.var(samples_x, ddof=n_x - 1)
    var_y = np.var(samples_y, ddof=n_y - 1)

    pooled_var = (var_x + var_y) / (n_x + n_y - 2) * (1.0 / n_x + 1.0 / n_y)

    test_statistic = (mean_x - mean_y - diff) / np.sqrt(pooled_var)

    null_dist = scipy.stats.t(n_x + n_y - 2)

    if tail == "both":
        p_value = 2.0 * null_dist.cdf(-abs(test_statistic))

    elif tail == "left":
        p_value = null_dist.cdf(test_statistic)

    else:
        p_value = null_dist.sf(test_statistic)

    return test_statistic, p_value

def _test():
    sample_std = 6
    test_x = [12, 10, -5, -5, 0, 0, 0]
    test_y = [12, 11, -5, -4, 0, 1, -1]
    diffs = [0, 0, 0, -1, 0, -1, 1]
    for tail, tail_scipy in zip(
        ["both", "left", "right"], ["two-sided", "less", "greater"]
    ):
        for mean_x, mean_y, diff in zip(test_x, test_y, diffs):
            sample_x = mean_x + sample_std * np.random.randn(200)
            sample_y = mean_y + sample_std * np.random.randn(100)
            res = two_sample_t_test_equal_var(sample_x, sample_y, diff=diff, tail=tail)
            scipy_res = scipy.stats.ttest_ind(
                sample_x, sample_y, equal_var=True, alternative=tail_scipy
            )

            print(tail, res, scipy_res)
            if np.isclose(0, diff):
                assert np.allclose(res, scipy_res)

            res_welch = welch_t_test(sample_x, sample_y, diff=diff, tail=tail)
            scipy_welch_res = scipy.stats.ttest_ind(
                sample_x, sample_y, equal_var=False, alternative=tail_scipy
            )
            print(tail, res_welch, scipy_welch_res)
            if np.isclose(0, diff):
                assert np.allclose(res_welch, scipy_welch_res)

if __name__ == "__main__":
    _test()


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

cfs_to_taf = 2.29568411*10**-5 * 86400 / 1000

def autocorr(x,k):
  '''returns the lag-k autocorrelation of vector x'''
  return np.corrcoef(x[:len(x)-k], x[k:])[0,1]

def thomasfiering(x1, x2, N):
  '''Lag-1 model for two sites. use historical data x
  to generate a synthetic sequence of N timesteps.
  Assumes x is lognormally distributed.'''
  # being lazy here, do it in a loop instead
  x1 = np.log(x1) # log-space avoids negative values
  m1 = x1.mean()
  s1 = x1.std()
  r1 = autocorr(x1,1)
  Q1 = np.zeros(N) # initialize
  Q1[0] = np.random.normal(m1,s1,1)

  x2 = np.log(x2) # log-space avoids negative values
  m2 = x2.mean()
  s2 = x2.std()
  r2 = autocorr(x2,1)
  Q2 = np.zeros(N) # initialize
  Q2[0] = np.random.normal(m2,s2,1)

  Sigma = np.corrcoef(x1, x2)

  for i in range(1,N):
    Z = np.random.multivariate_normal([0,0], Sigma, 1)
    Q1[i] = m1 + r1*(Q1[i-1] - m1) + Z[0,0]*s1*np.sqrt(1-r1**2)
    Q2[i] = m2 + r2*(Q2[i-1] - m2) + Z[0,1]*s2*np.sqrt(1-r2**2)

  return np.exp(Q1), np.exp(Q2)

# read in data and upscale to annual. rename the column we're going to use.
# this is an example of "method chaining" with pandas dataframes
# it's not required to use multiple lines, but usually makes it more readable.
dfF = (pd.read_csv('data/FOL.csv', index_col=0, parse_dates=True)
         .rename(columns={'FOL_INFLOW_CFS':'inflow'}))
dfS = (pd.read_csv('data/SHA.csv', index_col=0, parse_dates=True)
         .rename(columns={'SHA_INFLOW_CFS':'inflow'}))

dfS.inflow *= cfs_to_taf
dfF.inflow *= cfs_to_taf
dfS = dfS.resample('AS-OCT').sum()
dfF = dfF.resample('AS-OCT').sum()

# generate synthetic (input numpy arrays)
Q1, Q2 = thomasfiering(dfS.inflow.values, dfF.inflow.values, N=200)

# compare spatial correlation (this is in real space, not log)
print('Historical r = %0.3f' % np.corrcoef(dfS.inflow.values, dfF.inflow.values)[0,1])
print('Synthetic r = %0.3f' % np.corrcoef(Q1, Q2)[0,1])


"""
Exercise 2
Write a program that asks the user for two expressions and then graphs them
both,as follows:
>>> expr1 = input('Enter your first expression in terms of x and y: ')
>>> expr2 = input('Enter your second expression in terms of x and y: ')
Once you've complete this, enhance your program to print the solution - the pair
of x and y values that satisfies both equation. This will also be the spot
where the two lines on the graph intersect. (Hint: Refer to how we used to the
solve () function earlier to find the solution of a system of two linear
equations)

"""

from sympy import plot, solve, sympify
from sympy.core.sympify import SympifyError
from sympy import Symbol

expr1 = input("Enter your first expression in terms of x and y: ")
expr2 = input("Enter your second expression in terms of x and y: ")

try:
    expr1 = sympify(expr1)
    expr2 = sympify(expr2)

except SympifyError:
    print("Invalid input")

x = Symbol("x")
y = Symbol("y")

# plot(expr1,expr2)
print(solve(expr1, expr2, dict=True))


# A perfect number is a number for which the sum of its proper divisors is exactly equal to the number. For example, the sum of the proper divisors of 28 would be 1 + 2 + 4 + 7 + 14 = 28, which means that 28 is a perfect number.

# A number n is called deficient if the sum of its proper divisors is less than n and it is called abundant if this sum exceeds n.

# As 12 is the smallest abundant number, 1 + 2 + 3 + 4 + 6 = 16, the smallest number that can be written as the sum of two abundant numbers is 24. By mathematical analysis, it can be shown that all integers greater than 28123 can be written as the sum of two abundant numbers. However, this upper limit cannot be reduced any further by analysis even though it is known that the greatest number that cannot be expressed as the sum of two abundant numbers is less than this limit.

# Find the sum of all the positive integers which cannot be written as the sum of two abundant numbers.
import numpy as np
from utils.common import divisors
max_num = 28123
abundant_nums = np.zeros(max_num, dtype=np.int8)
for i in range(12, max_num):
    if sum(divisors(i, proper=True)) > i:
        abundant_nums[i] = 1
# print(np.bincount(abundant_nums))

nums = np.ones(max_num, dtype=np.int8)
for i in range(24, max_num):
    for a in range(12, i//2 + 1):
        if abundant_nums[a] == 0:continue
        if abundant_nums[i-a] == 1:
            # this is the sum of two abundant numbers so remove from list by setting to 0
            nums[i] = 0
            break
num_sum = 0
for i, n in enumerate(nums):
    if n == 1:
        num_sum += i
print(num_sum)


import math
import numpy as np

def distance(c1, c2, indexes=None, weights=None, nan_value=None):
    """ Calculate the Euclidean distance between two clusters
    :param c1 The first cluster
    :param c2 The second cluster
    :param indexes the indexes to compute the Euclidean distance
    :param weights the to weight the some of the values, default is None where all indexes have equal weights
    :param nan_value the value that will replace nan in the clusters' serie. Default is None which does not replace nan
    """
    s1 = c1.serie if indexes is None else [c1.serie[i] for i in indexes]
    s2 = c2.serie if indexes is None else [c2.serie[i] for i in indexes]
    assert len(s1) == len(s2) and len(s1) > 0
    if nan_value is not None:
        # Replace nan values in the series
        s1 = [ nan_value if np.isnan(x) else x for x in s1]
        s2 = [ nan_value if np.isnan(x) else x for x in s2]
    # Handle weights
    if weights is None:
        weights = [1 for _ in range(len(s1))]
    else:
        assert len(weights) == len(s1)
    return np.sqrt((np.multiply(weights, np.power(np.subtract(s1, s2), 2))).sum())

def get_indexes (minIndex, nClusters):
    """ Get the index of the clusters based on the index of the minimum distance between clusters
    """
    index1 = 0
    while minIndex >= (nClusters - 1):
        minIndex -= nClusters - 1
        nClusters -= 1
        index1 += 1
    index2 = minIndex % (nClusters - 1)
    return (index1, index2 + index1 + 1)


#
# demo_statistics.py
#

import numpy
import random
import statistics
import matplotlib.pyplot as plt

'''
Calculate the mean, mode, median, standard deviation and variance.
'''

data = numpy.random.normal(1, 0.5, 10000)

mean = statistics.mean(data)
mode = statistics.mode(data)
median = statistics.median(data)
std = statistics.standard_deviation(data)
variance = statistics.variance(data)

print('')
print('Mean: ' + str(mean))
print('Mode: ' + str(mode))
print('Median: ' + str(median))
print('Standard Variance: ' + str(std))
print('Variance: ' + str(variance))

plt.xlabel('value')
plt.ylabel('count')
plt.hist(data, 50)
plt.show()

'''
Calculate the covariance of uncorrelated data.
'''

ages = numpy.random.normal(50.0, 10.0, 1000)
income = numpy.random.normal(100000.0, 75000.0, 1000)

covariance = statistics.covariance(ages, income, is_sample=True)
correlation = statistics.correlation(ages, income, is_sample=True)

print('')
print('Uncorrelated data.')
print('Covariance: ' + str(covariance))
print('Correlation: ' + str(correlation))

plt.xlabel('age')
plt.ylabel('income')
plt.scatter(ages, income)
plt.show()

'''
Calculate the covariance and correlation of data known to be correlated.
'''

ages = numpy.random.normal(50.0, 10.0, 1000)
income = [(n*1000+random.uniform(0, n/1000)*500000) for n in ages]

covariance = statistics.covariance(ages, income, is_sample=True)
correlation = statistics.correlation(ages, income, is_sample=True)

print('')
print('Correlated data.')
print('Covariance: ' + str(covariance))
print('Correlation: ' + str(correlation))

plt.xlabel('age')
plt.ylabel('income')
plt.scatter(ages, income)
plt.show()

print('')


# coding: utf-8

# In[2]:

import numpy as np
from scipy import linalg
arr = np.array([[1, 2],[3, 4]])
##矩阵行列式
print("矩阵行列式：",linalg.det(arr))
print("矩阵的逆：",linalg.inv(arr))

# In[5]:

#奇异值分解
arr = np.arange(9).reshape((3, 3)) + np.diag([1, 0, 1])
uarr, spec, vharr = linalg.svd(arr)
print(spec)
sarr = np.diag(spec)
svd_mat = uarr.dot(sarr).dot(vharr)
print(svd_mat)
np.allclose(arr,svd_mat)

# In[10]:

##傅里叶变换
##优化
from scipy import optimize
def f(x):
    return x**2 + 10*np.sin(x)
import matplotlib.pyplot as plt
x = np.arange(-10, 10, 0.1)
plt.plot(x, f(x))
plt.show()
##bfgs依赖于初始点，有可能得到局部最小
optimize.fmin_bfgs(f, 0)

# In[12]:

optimize.fmin_bfgs(f, 3)

# In[13]:

##全局最优
optimize.basinhopping(f, 0)

# In[15]:

#计算函数的根
#1 只求的一个
root = optimize.fsolve(f, 1)
root

# In[16]:

##曲线拟合
xdata = np.linspace(-10, 10, num=20)
ydata = f(xdata) + np.random.randn(xdata.size)
#假设满足函数f2，然后求a、b
def f2(x, a, b):
     return a*x**2 + b*np.sin(x)
guess = [2, 2]
params, params_covariance = optimize.curve_fit(f2, xdata, ydata, guess)
params

# In[27]:

#统计
a = np.random.normal(size=1000)
bins = np.arange(-4, 5)
print(bins)
histogram = np.histogram(a, bins=bins, normed=True)[0]
print(histogram)
bins = 0.5*(bins[1:] + bins[:-1])
print(bins)
from scipy import stats
#pdf概率密度函数probability density function
b = stats.norm.pdf(bins)
print("pdf:",b)
plt.plot(bins, histogram)
plt.plot(bins, b)
plt.show()
loc, std = stats.norm.fit(a)
print("loc:"+str(loc)+"std:"+str(std))
#中位数
np.median(a)

# In[28]:

#50百分位
stats.scoreatpercentile(a, 50)

# In[29]:

#t检验
a = np.random.normal(0, 1, size=100)
b = np.random.normal(1, 1, size=10)
stats.ttest_ind(a, b)

# In[ ]:


"""""""""""
NumPy Coding

"""""""""""

import numpy as np

NumP_Array = np.array([[1,2,3],[4,6,7]])

NP1 = np.array([[1,3],[4,5]])

NP2 = np.array([[3,4],[5,7]])

MNP = NP1@NP2

MNP3 = np.dot(NP1,NP2)

MNP2 = NP1*NP2

MNP4 = np.multiply(NP1,NP2)

Sum1 = NP1+NP2

Sub1 = NP1-NP2

Sub2 = np.subtract(NP1,NP2)

np.sum(NP1)

Broad_Nump = NP1+3

NP3 = np.array([[3,4]])

NP1+NP3

D = np.divide([12,14,16],5)

D = np.floor_divide([12,14,16],5)

np.math.sqrt(10)

ND = np.random.standard_normal((3,4))

UD = np.random.uniform(1,12,(3,4))

# Generate Float No.

np.random.rand()

# Generate Integer No.

Random_Ar= np.random.randint(1,50,(2,5))

Ze = np.zeros((3,4))

Ones = np.ones((3,4))

Filter_Ar = np.logical_and(Random_Ar>30,Random_Ar<50)

F_Random_Ar = Random_Ar[Filter_Ar]

Data_N = np.array([1,3,4,5,7,9])

Mean_N = np.mean(Data_N)

Median_N = np.median(Data_N)

Var_N = np.var(Data_N)

SD_N = np.std(Data_N)

NumP_Array = np.array([[1,2,3],[4,6,7]])

Var_Nump = np.var(NumP_Array,axis=1)

Var_Nump2 = np.var(NumP_Array,axis=0)


import numpy as np

def _compute_c_matrix(N):
    C = np.zeros((N, N))

    C[0, :] = np.sqrt(1.0 / N)
    for k in range(1, N):
        for j in range(N):
            C[k, j] = np.cos(np.pi * k * (2 * j + 1) / (2.0 * N)) * np.sqrt(2.0 / N)

    return C

def _dct(A):
    if A.ndim != 1:
        raise ValueError("matrix 'A' must be 1-dimensional")

    N = A.shape[0]
    return _compute_c_matrix(N).dot(A)

def _dct2(A):
    if A.ndim != 2:
        raise ValueError("matrix 'A' must be 2-dimensional")

    N, M = A.shape
    if N != M:
        raise ValueError(f"Matrix 'A' ({N}x{M}) must be squared")

    C = _compute_c_matrix(N)
    return C.dot(A).dot(C.T)

def dct(A):
    """
    params: A matrix Nx1
    output: compute dct 1D of the matrix A
    """
    N = A.shape[0]
    C = np.zeros(N, dtype=np.float64)

    for k in range(0, N):
        a_k = np.sqrt(1.0 / N) if k == 0 else np.sqrt(2.0 / N)

        sum = 0
        for j in range(N):
            sum = sum + A[j] * np.cos(k * np.pi * (2 * j + 1) / (2.0 * N))

        C[k] = a_k * sum
    return C

def dct2(A):
    """
    params: A matrix NxM
    output: compute dct 2D of the matrix A
    """
    N, M = A.shape
    C = np.zeros((N, M), dtype=np.float64)

    for i in range(N):
        C[i] = dct(A[i])

    for j in range(M):
        C[:, j] = dct(C[:, j])
    return C


####################################################################################################################################
##
##  Description:
##      +   Return the natural logarithm of an ndim-dimensional Multivariate Normal (MVN)
##          probability density function (PDF) with the Mean and Covariance Matrix as defined below.
##          Reference: https://en.wikipedia.org/wiki/Multivariate_normal_distribution
##  Input:
##      +   point:      The input 64-bit real-valued vector of length ndim,
##                      at which the natural logarithm of objective function is computed.
##  Output:
##      +   logFunc:    A 64-bit real scalar number representing the natural logarithm of the objective function.
##  Author:
##      +   Computational Data Science Lab, Monday 9:03 AM, May 16 2016, ICES, UT Austin
##  Visit:
##      +   https://www.cdslab.org/paramonte
##
####################################################################################################################################

import numpy as np

# The number of dimensions of the domain of the objective function.

NDIM = 4

# This is the mean of the MVN distribution.

MEAN =  [0.0,0.0,0.0,0.0]

# This is the covariance matrix of the MVN distribution.

COVMAT =    [ [1.0,0.5,0.5,0.5]
            , [0.5,1.0,0.5,0.5]
            , [0.5,0.5,1.0,0.5]
            , [0.5,0.5,0.5,1.0]
            ]

# This is the inverse of the covariance matrix of the MVN distribution.

INVCOV = np.linalg.inv(COVMAT)

# This is the log of the coefficient used in the definition of the MVN.

MVN_COEF = NDIM * np.log( 1. / np.sqrt(2.*np.pi) ) + np.log( np.sqrt(np.linalg.det(INVCOV)) )

def getLogFunc(point):
    """
    Return the natural logarithm of an NDIM-dimensional Multivariate Normal distribution
    with the mean and covariance matrix as given in the above.
    Reference: https://en.wikipedia.org/wiki/Multivariate_normal_distribution
    """
    normedPoint = MEAN - point
    return MVN_COEF - 0.5 * ( np.dot(normedPoint,np.matmul(INVCOV,normedPoint)) )


import numpy as np
import sympy as sp
from sympy.core import S, Dummy
from sympy.functions.special.gamma_functions import gamma
from sympy.polys.orthopolys import (legendre_poly, laguerre_poly,
                                    hermite_poly, jacobi_poly)
from sympy.polys.rootoftools import RootOf

def gauss_jacobi(n, alpha, beta, n_digits):
    r"""
    Computes the Gauss-Jacobi quadrature [1]_ points and weights.

    The Gauss-Jacobi quadrature of the first kind approximates the integral:

    .. math::
        \int_{-1}^1 (1-x)^\alpha (1+x)^\beta f(x)\,dx \approx
            \sum_{i=1}^n w_i f(x_i)

    The nodes `x_i` of an order `n` quadrature rule are the roots of
    `P^{(\alpha,\beta)}_n` and the weights `w_i` are given by:

    .. math::
        w_i = -\frac{2n+\alpha+\beta+2}{n+\alpha+\beta+1}
              \frac{\Gamma(n+\alpha+1)\Gamma(n+\beta+1)}
              {\Gamma(n+\alpha+\beta+1)(n+1)!}
              \frac{2^{\alpha+\beta}}{P'_n(x_i)
              P^{(\alpha,\beta)}_{n+1}(x_i)}

    Parameters
    ==========

    n : the order of quadrature

    alpha : the first parameter of the Jacobi Polynomial, `\alpha > -1`

    beta : the second parameter of the Jacobi Polynomial, `\beta > -1`

    n_digits : number of significant digits of the points and weights to return

    Returns
    =======

    (x, w) : the ``x`` and ``w`` are lists of points and weights as Floats.
             The points `x_i` and weights `w_i` are returned as ``(x, w)``
             tuple of lists.

    Examples
    ========

    >>> from sympy import S
    >>> from sympy.integrals.quadrature import gauss_jacobi
    >>> x, w = gauss_jacobi(3, S.Half, -S.Half, 5)
    >>> x
    [-0.90097, -0.22252, 0.62349]
    >>> w
    [1.7063, 1.0973, 0.33795]

    >>> x, w = gauss_jacobi(6, 1, 1, 5)
    >>> x
    [-0.87174, -0.5917, -0.2093, 0.2093, 0.5917, 0.87174]
    >>> w
    [0.050584, 0.22169, 0.39439, 0.39439, 0.22169, 0.050584]

    See Also
    ========

    gauss_legendre, gauss_laguerre, gauss_hermite, gauss_gen_laguerre, gauss_chebyshev_t, gauss_chebyshev_u, gauss_lobatto

    References
    ==========

    .. [1] https://en.wikipedia.org/wiki/Gauss%E2%80%93Jacobi_quadrature
    .. [2] http://people.sc.fsu.edu/~jburkardt/cpp_src/jacobi_rule/jacobi_rule.html
    .. [3] http://people.sc.fsu.edu/~jburkardt/cpp_src/gegenbauer_rule/gegenbauer_rule.html
    """
    x = Dummy("x")
    p = jacobi_poly(n, alpha, beta, x, polys=True)
    pd = p.diff(x)
    pn = jacobi_poly(n+1, alpha, beta, x, polys=True)
    xi = []
    wi = []
    for r in p.real_roots():
        if isinstance(r, RootOf):
            r = r.eval_rational(S(1)/10**(n_digits+2))
        xi.append(r.n(n_digits))
        wi.append((
            - (2*n+alpha+beta+2) / (n+alpha+beta+S.One) *
            (gamma(n+alpha+1)*gamma(n+beta+1)) /
            (gamma(n+alpha+beta+S.One)*gamma(n+2)) *
            2**(alpha+beta) / (pd.subs(x, r) * pn.subs(x, r))).n(n_digits))
    return xi, wi

alpha = 0
beta  = 0
digits = 36
npoints = list(range(1,17))

for n in npoints:
    xi, wi = gauss_jacobi(n, alpha, beta, digits)

    print('\nN = %3d'%(n))
    for i in range(0,n):
        print('%3d %40.36f %40.36f'%(i+1,xi[i],wi[i]))


#import libraries
import numpy as np
import matplotlib.pyplot as plt

class LogisticRegression:

    X = None
    y = None
    C = None
    sig = None

    def fit(self, X, y):
        """Fit the model according to the given training data

        Args:
            X ([type]): [description]
            y ([type]): [description]

        Returns:
            LogisticRegression: [description]
        """
        one = np.ones(X.shape[0]).reshape(-1,1)
        self.X = X#np.concatenate((one, X), axis=1)
        self.y = y
        self.C = self.__getCoefficients(self.X, self.y)
        return self

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

    def __getCoefficients(self,X,y):
        xDotx = np.dot(X.T,X)
        xDotxInverse = np.linalg.inv(xDotx)
        xDotxInverseDotXT = np.dot(xDotxInverse,X.T)
        return np.dot(xDotxInverseDotXT,y)

    def __pred_single(self,y_score):
        if y_score <= 0.5:
            return 0
        else:
            return 1

    def predict_log_proba(self,X):
        """[summary]

        Args:
            X ([type]): [description]

        Returns:
            [type]: [description]
        """
        one = np.ones(X.shape[0]).reshape(-1,1)
        X = X#np.concatenate((one, X), axis=1)
        self.sig = self.__sigmoid(np.dot(X,self.C))
        return self.sig

    def predict(self,X):
        """Predict class labels for samples in X"""
        sig = self.predict_log_proba(X)
        return np.array([self.__pred_single(i) for i in sig])

    def score(self,y_test,y_pred):
        """Return the mean accuracy on the given test data and labels"""
        return (sum(y_test == y_pred) / len(y_pred))*100

    def __separate_labels(self,labels, points):
        data = {}
        for id,l in enumerate(labels):
            if l not in data.keys():
                data[l] = [list(points[id])]
            else:
                data[l].append(list(points[id]))

        for k,v in data.items():
            data[k] = np.array(v,dtype=object)
        return data

    def make_plot(self,y,X,title='Title'):
        """makes plot of X and y

        Args:
            y (array(float)): vector of floats
            X (array(float)): vector of floats
            title (str, optional): title for plot. Defaults to 'Title'.
        """
        data = self.__separate_labels(y, X)
        for k,v in data.items():
            plt.scatter(x=v[:,0],y=v[:,1],label=str(k))
        plt.legend()
        plt.xlabel('x1');
        plt.ylabel('x2');
        plt.title(title);


import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
from matplotlib.colors import Normalize

def hist_equ(raw_img=None, file_name=None):
    """
    Implement the histogram equalization to the input images Q3_1_1.tif and Q3_1_2.tif
    """

    if raw_img is None:
        raw_img = cv.imread(file_name, cv.IMREAD_GRAYSCALE)

    norm = Normalize(vmin=0, vmax=255)
    L = 2 ** 8
    bins = range(L + 1)
    # row, col = raw_img.shape

    # input_hist = np.zeros(L, int)
    # for i in raw_img.flat:
    #     input_hist[i] += 1

    # input_hist = histogram(raw_img)
    input_hist, _ = np.histogram(raw_img.flat, bins=bins, density=True)
    # print(file_name, 'raw', np.count_nonzero(input_hist))

    # s = np.zeros(L, int)
    # for k in range(L):
    #     s[k] = (L - 1) * sum(input_hist[:k + 1])

    s = np.array([(L - 1) * sum(input_hist[:k + 1]) for k in range(L)])

    out_img = np.array([s[r] for r in raw_img], int).reshape(raw_img.shape)
    # output_hist = histogram(out_img)
    output_hist, _ = np.histogram(out_img.flat, bins=bins, density=True)
    # print(file_name, 'equalized', np.count_nonzero(output_hist))

    # %% plots
    '''
    plt.subplot(121)
    plt.imshow(raw_img, cmap='gray', norm=norm)
    plt.title("Raw " + file_name)

    plt.subplot(122)
    plt.imshow(out_img, cmap='gray', norm=norm)
    plt.title("Equalized " + file_name)
    # plt.savefig(file_name + "_comparison.png")
    plt.show()

    plt.title("Histogram of " + file_name)
    plt.bar(range(L), input_hist)
    plt.bar(range(L), output_hist)
    plt.legend(('raw image', 'equalized image'))
    # plt.savefig(file_name + "_histogram.png")
    plt.show()

    plt.plot(range(L), s)
    plt.title("Histogram equalization transformation for " + file_name)
    plt.xlabel('$r_k$')
    plt.ylabel('$s_k$')
    plt.show()
    '''

    return out_img, output_hist, input_hist, s

# %%

*_, trans_1 = hist_equ(file_name="Q3_1_1.tif")
*_, trans_2 = hist_equ(file_name="Q3_1_2.tif")

plt.plot(range(2 ** 8), trans_1)
plt.plot(range(2 ** 8), trans_2)
plt.title("Histogram equalization transformation")
plt.xlabel('$r_k$')
plt.ylabel('$s_k$')
plt.legend(('Q3_1_1.tif', 'Q3_1_2.tif'))
# plt.savefig("Q3_1_trans.png")
plt.show()


import math
import numpy as np
import scipy.stats

# Confidence interval (CI) for mean

def ci_mean(alpha, sample=[], sample_mean=False, n=False, true_sigma=False, sample_sigma=False, verbose=False):
    # when there is a vector of values i.e. sample itself
    if len(sample) != 0:
        sample = np.array(sample)
        n = len(sample)
        sample_mean = np.mean(sample)
        sample_sigma = np.std(sample)
    # known true (theoretical) sigma
    if true_sigma != False:
        z = scipy.stats.norm.ppf(1 - alpha)
        lower_bound = sample_mean - z * true_sigma / math.sqrt(n)
        upper_bound = sample_mean + z * true_sigma / math.sqrt(n)
        margin_error = z * true_sigma / math.sqrt(n)
        if verbose == True:
            print(f'{round((1-alpha) * 100)}% Confidence Interval (CI) for mean is between {round(lower_bound, 2)} and\
    {round(upper_bound, 2)}')
            print(f'Z statistics with n={n} and alpha={alpha} is equal to {round(z, 2)}')
            print(f'CI width is {round(upper_bound - lower_bound, 2)}')
            print(f'Margin of Error is {round(margin_error, 2)}')
            print(f'Point estimate {round((lower_bound + upper_bound) / 2, 2)}')
        return lower_bound, upper_bound
    # unknown true sigma but known sig
    elif sample_sigma != False:
        t = scipy.stats.t.ppf(1 - alpha / 2, n - 1)
        lower_bound = sample_mean - t * sample_sigma / math.sqrt(n)
        upper_bound = sample_mean + t * sample_sigma / math.sqrt(n)
        margin_error = t * sample_sigma / math.sqrt(n)
        if verbose == True:
            print(f'{round((1-alpha) * 100)}% Confidence Interval for mean is between {round(lower_bound, 2)} and\
    {round(upper_bound, 2)}')
            print(f't (student) statistics with n={n} and alpha={alpha} is equal to {round(t, 2)}')
            print(f'CI width is {round(upper_bound - lower_bound, 2)}')
            print(f'Margin of Error is {round(margin_error, 2)}')
            print(f'Point estimate {round((lower_bound + upper_bound) / 2, 2)}')
        return lower_bound, upper_bound
    else:
        print('Some data is missing. Check input parameters')

# CI for variance

def ci_variance(n, alpha, sample=[], sample_var=False, true_mean=False, verbose=False):
    # when there is a vector of values i.e. sample itself
    if len(sample) != 0:
        sample = np.array(sample)
        n = len(sample)
        sample_var = np.var(sample)

    # known true variance and there is a sample itself
    if (true_mean != False) and (len(sample) != 0):
        chi_squared_right = scipy.stats.chi2.ppf(1 - alpha / 2, n) # bigger number
        chi_squared_left = scipy.stats.chi2.ppf(alpha / 2, n) # smaller number
        numerator = sum((np.array(sample) - true_mean) ** 2)
        lower_bound = numerator / chi_squared_right
        upper_bound = numerator / chi_squared_left
        if verbose == True:
            print(f'{round((1-alpha) * 100)}% Confidence Interval (CI) for variance is between {round(lower_bound, 2)} and\
    {round(upper_bound, 2)}')
            print(f'Chi-square statistics with n={n} and alpha/2={alpha / 2} is equal to {round(chi_squared_right, 2)}')
            print(f'Chi-square statistics with n={n} and 1-alpha/2={1 - alpha / 2} is equal to {round(chi_squared_left, 2)}')
            print(f'CI width is {round(upper_bound - lower_bound, 2)}')
            print(f'Point estimate {round((upper_bound + lower_bound) / 2, 2)}')
        return lower_bound, upper_bound
    # unknown true variance but known sample variance
    elif (sample_var != False) and (true_mean == False):
        chi_squared_right = scipy.stats.chi2.ppf(1 - alpha / 2, n - 1) # bigger number
        chi_squared_left = scipy.stats.chi2.ppf(alpha / 2, n - 1) # smaller number
        lower_bound = (n - 1) * sample_var / chi_squared_right
        upper_bound = (n - 1) * sample_var / chi_squared_left
        if verbose == True:
            print(f'{round((1-alpha) * 100)}% Confidence Interval (CI) for variance is between {round(lower_bound, 2)} and\
    {round(upper_bound, 2)}')
            print(f'Chi-square statistics with n={n} and alpha/2={alpha / 2} is equal to {round(chi_squared_right, 2)}')
            print(f'Chi-square statistics with n={n} and 1-alpha/2={1 - alpha / 2} is equal to {round(chi_squared_left, 2)}')
            print(f'CI width is {round(upper_bound - lower_bound, 2)}')
            print(f'Point estimate {round((upper_bound + lower_bound) / 2, 2)}')
        return (n - 1) * sample_var / chi_squared_right, (n - 1) * sample_var / chi_squared_left
    else:
        print('Some data is missing. Check input parameters')

# CI for probability (share)

def ci_probs(alpha, sample=[], p_hat=False, n=False, verbose=False):
    # when there is a vector of values i.e. sample itself
    if len(sample) != 0:
        sample = np.array(sample)
        n = len(sample)
        p_hat = np.mean(sample)

    if (p_hat != False) and (n != False):
        z = scipy.stats.norm.ppf(1 - alpha / 2)
        lower_bound = p_hat - z * math.sqrt(p_hat * (1 - p_hat) / n)
        upper_bound = p_hat + z * math.sqrt(p_hat * (1 - p_hat) / n)
        margin_error = z * math.sqrt(p_hat * (1 - p_hat) / n)
        if verbose == True:
            print(f'{round((1-alpha) * 100, 1)}% Confidence Interval (CI) for mean is between {round(lower_bound, 2)} and\
        {round(upper_bound, 2)}')
            print(f'Z statistics with n={n} and 1 - alpha/2 ={1 - alpha / 2} is equal to {round(z, 2)}')
            print(f'CI width is {round(upper_bound - lower_bound, 2)}')
            print(f'Margin of Error is {round(margin_error, 2)}')
            print(f'Point estimate {round((lower_bound + upper_bound) / 2, 2)}')
        return lower_bound, upper_bound
    else:
        print('Some data is missing. Check input parameters')

# CI for means difference

def ci_mean_dif(alpha, sample_x=[], sample_y=[], sample_mean_x=False, sample_mean_y=False, n_x=False, n_y=False,
                true_var_both=False, true_var_x=False, true_var_y=False,
               sample_var_x=False, sample_var_y=False, verbose=False):

    if (len(sample_x) != 0) and (len(sample_y) != 0):
        sample_x = np.array(sample_x)
        sample_y = np.array(sample_y)
        sample_mean_x = np.mean(sample_x)
        sample_mean_y = np.mean(sample_y)
        n_x = len(sample_x)
        n_y = len(sample_y)
        sample_var_x = np.var(sample_x)
        sample_var_y = np.var(sample_y)

    # both true variances are known and equal
    if true_var_both !=False:
        z = scipy.stats.norm.ppf(1 - alpha / 2)
        print('both true variances are known and equal')
        lower_bound = sample_mean_x - sample_mean_y - z * true_var_both / math.sqrt(n_x + n_y)
        upper_bound = sample_mean_x - sample_mean_y + z * true_var_both / math.sqrt(n_x + n_y)
        margin_error = z * true_var_both / math.sqrt(n_x + n_y)
        if verbose == True:
            print(f'{round((1-alpha) * 100, 1)}% Confidence Interval (CI) for difference of means is between\
    {round(lower_bound, 2)} and {round(upper_bound, 2)}')
            print(f'Z statistics with 1 - alpha/2 ={1 - alpha / 2} is equal to {round(z, 2)}')
            print(f'CI width is {round(upper_bound - lower_bound, 2)}')
            print(f'Margin of Error is {round(margin_error, 2)}')
            print(f'Point estimate {round((lower_bound + upper_bound) / 2, 2)}')
        return lower_bound, upper_bound

    # both true variances are known and not equal
    elif (true_var_x != False) and (true_var_y != False):
        z = scipy.stats.norm.ppf(1 - alpha / 2)
        print('both true variances are known and not equal')
        lower_bound = sample_mean_x - sample_mean_y - z * math.sqrt(true_var_x/n_x + true_var_y/n_y)
        upper_bound = sample_mean_x - sample_mean_y + z * math.sqrt(true_var_x/n_x + true_var_y/n_y)
        margin_error = z * math.sqrt(true_var_x/n_x + true_var_y/n_y)
        if verbose == True:
            print(f'{round((1-alpha) * 100, 1)}% Confidence Interval (CI) for difference of means is between\
    {round(lower_bound, 2)} and {round(upper_bound, 2)}')
            print(f'Z statistics with 1 - alpha/2 ={1 - alpha / 2} is equal to {round(z, 2)}')
            print(f'CI width is {round(upper_bound - lower_bound, 2)}')
            print(f'Margin of Error is {round(margin_error, 2)}')
            print(f'Point estimate {round((lower_bound + upper_bound) / 2, 2)}')

        return lower_bound, upper_bound
    # both variances are unknown but equal
    elif (sample_var_x == sample_var_y) and (sample_var_x != False):
        t = scipy.stats.t.ppf(1 - alpha / 2, n_x + n_y - 2)
        print('both variances are unknown but equal')
        lower_bound = sample_mean_x - sample_mean_y - t * (math.sqrt((n_x - 1)*sample_var_x + (n_y - 1)*sample_var_y)) /\
    math.sqrt((n_x * n_y * (n_x + n_y - 2)) / (n_x + n_y))
        upper_bound = sample_mean_x - sample_mean_y + t * (math.sqrt((n_x - 1)*sample_var_x + (n_y - 1)*sample_var_y)) /\
    math.sqrt((n_x * n_y * (n_x + n_y - 2)) / (n_x + n_y))
        margin_error = t * (math.sqrt((n_x - 1)*sample_var_x + (n_y - 1)*sample_var_y)) /\
    math.sqrt((n_x * n_y * (n_x + n_y - 2)) / (n_x + n_y))

        if verbose == True:
            print(f'{round((1-alpha) * 100, 1)}% Confidence Interval (CI) for difference of means is between\
    {round(lower_bound, 2)} and {round(upper_bound, 2)}')
            print(f't statistics with 1 - alpha/2 ={1 - alpha / 2} is equal to {round(t, 2)}')
            print(f'CI width is {round(upper_bound - lower_bound, 2)}')
            print(f'Margin of Error is {round(margin_error, 2)}')
            print(f'Point estimate {round((lower_bound + upper_bound) / 2, 2)}')

        return sample_mean_x - sample_mean_y - t * (math.sqrt((n_x - 1)*sample_var_x + (n_y - 1)*sample_var_y)) /\
    math.sqrt((n_x * n_y * (n_x + n_y - 2)) / (n_x + n_y)),\
    sample_mean_x - sample_mean_y + t * (math.sqrt((n_x - 1)*sample_var_x + (n_y - 1)*sample_var_y)) /\
    math.sqrt((n_x * n_y * (n_x + n_y - 2)) / (n_x + n_y))

    # both variances are unknown and unequal
    elif (sample_var_x != False) and (sample_var_y != False) and (sample_var_x != sample_var_y):
        t = scipy.stats.t.ppf(1 - alpha / 2, n_x + n_y - 2)
        print('both variances are unknown and unequal')
        upper_bound = sample_mean_x - sample_mean_y - t * math.sqrt(sample_var_x/n_x + sample_var_x/n_y)
        lower_bound = sample_mean_x - sample_mean_y + t * math.sqrt(sample_var_x/n_x + sample_var_x/n_y)
        margin_error = t * math.sqrt(sample_var_x/n_x + sample_var_x/n_y)

        if verbose == True:
            print(f'{round((1-alpha) * 100, 1)}% Confidence Interval (CI) for difference of means is between\
    {round(lower_bound, 2)} and {round(upper_bound, 2)}')
            print(f't statistics with 1 - alpha/2 ={1 - alpha / 2} is equal to {round(t, 2)}')
            print(f'CI width is {round(upper_bound - lower_bound, 2)}')
            print(f'Margin of Error is {round(margin_error, 2)}')
            print(f'Point estimate {round((lower_bound + upper_bound) / 2, 2)}')
        return sample_mean_x - sample_mean_y - t * math.sqrt(sample_var_x/n_x + sample_var_x/n_y),\
    sample_mean_x - sample_mean_y + t * math.sqrt(sample_var_x/n_x + sample_var_x/n_y)
    else:
        print('Some data is missing. Check input parameters')

# CI for probabilities difference

def ci_probs_dif(alpha, sample_x=[], sample_y=[], p_hat_x=False, p_hat_y=False, n_x=False, n_y=False,
verbose=False):
    if (len(sample_x) != 0) and (len(sample_y) != 0):
        sample_x = np.array(sample_x)
        sample_y = np.array(sample_y)
        p_hat_x = np.mean(sample_x)
        p_hat_y = np.mean(sample_y)
        n_x = len(sample_x)
        n_y = len(sample_y)

    if (p_hat_x != False) and (p_hat_y != False) and (n_x != False) and (n_y != False):
        p_hat = (p_hat_x * n_x + p_hat_y * n_y) / (n_x + n_y)
        z = scipy.stats.norm.ppf(1 - alpha / 2)
        lower_bound = p_hat_x - p_hat_y - z * math.sqrt(p_hat * (1 - p_hat) * (1/n_x + 1/n_y))
        upper_bound = p_hat_x - p_hat_y + z * math.sqrt(p_hat * (1 - p_hat) * (1/n_x + 1/n_y))
        margin_error = z * math.sqrt(p_hat * (1 - p_hat) * (1/n_x + 1/n_y))
        if verbose == True:
            print(f'{round((1-alpha) * 100, 1)}% Confidence Interval (CI) for difference of probabilities is between\
    {round(lower_bound, 2)} and {round(upper_bound, 2)}')
            print(f'Z statistics with 1 - alpha/2 ={1 - alpha / 2} is equal to {round(z, 2)}')
            print(f'CI width is {round(upper_bound - lower_bound, 2)}')
            print(f'Margin of Error is {round(margin_error, 2)}')
            print(f'Point estimate {round((lower_bound + upper_bound) / 2, 2)}')
        return lower_bound, upper_bound

    else:
        print('Some data is missing. Check input parameters')

# CI for ratio of variances

def ci_vars_ratio(alpha, sample_x=[], sample_y=[], sample_var_x=False, sample_var_y=False, verbose=False):
    if (len(sample_x) != 0) and (len(sample_y) != 0):
        sample_x = np.array(sample_x)
        sample_y = np.array(sample_y)
        sample_var_x = np.var(sample_x)
        sample_var_y = np.var(sample_y)
        n_x = len(sample_x)
        n_y = len(sample_y)

    if (sample_var_x != False) and (sample_var_y != False):
        F_left = scipy.stats.f.ppf(alpha / 2, n_x - 1, n_y - 1) # smaller number
        F_right = scipy.stats.f.ppf(1 - alpha / 2, n_x - 1, n_y - 1) # bigger number
        lower_bound = (1 / F_left) * (sample_var_x / sample_var_y)
        upper_bound = (1 / F_right) * (sample_var_x / sample_var_y)
        if verbose == True:
            print(f'{round((1-alpha) * 100, 1)}% Confidence Interval (CI) for ratio of variances is between\
    {round(lower_bound, 2)} and {round(upper_bound, 2)}')
            print(f'F statistics with alpha/2 ={alpha / 2} is equal to {round(F_left, 2)}')
            print(f'F statistics with 1 - alpha/2 ={1 - alpha / 2} is equal to {round(F_right, 2)}')
            print(f'CI width is {round(upper_bound - lower_bound, 2)}')
            print(f'Point estimate {round((lower_bound + upper_bound) / 2, 2)}')
        return lower_bound, upper_bound
    else:
        print('Some data is missing. Check input parameters')


import math

from pyrr import Quaternion, Matrix44, Vector3

import numpy as np

print("#########################################")

#np.set_printoptions(precision=3)
np.set_printoptions(formatter={'float': '{: 8.3f}'.format})
#np.set_printoptions(suppress=True)

location_v = Vector3([5.0, 6.0, 7.0])
location_m = Matrix44.from_translation(location_v)

print("Location Matrix")

print("")

#print(location_m)
transform_flat = location_m.flatten()
transform_array = np.array(transform_flat, np.float32)
print(transform_array)

print("")

print("#########################################")

deg = -10
rad = (deg * np.pi / 180)
q_rot = Quaternion.from_x_rotation(rad)

q_array = np.array(q_rot, np.float32)

rotation_m = Matrix44(q_rot)

print("Rotation Matrix - X")

print("")

print(q_array)

#print(rotation_m)
transform_flat = rotation_m.flatten()
transform_array = np.array(transform_flat, np.float32)
print(transform_array)

print("")

print("#########################################")

deg = -10
rad = (deg * np.pi / 180)
q_rot = Quaternion.from_y_rotation(rad)

q_array = np.array(q_rot, np.float32)

rotation_m = Matrix44(q_rot)

print("Rotation Matrix - Y")

print("")

print(q_array)

#print(rotation_m)
transform_flat = rotation_m.flatten()
transform_array = np.array(transform_flat, np.float32)
print(transform_array)

print("")

print("#########################################")

deg = -10
rad = (deg * np.pi / 180)
q_rot = Quaternion.from_z_rotation(rad)

q_array = np.array(q_rot, np.float32)

rotation_m = Matrix44(q_rot)

print("Rotation Matrix - Z")

print("")

print(q_array)

#print(rotation_m)
transform_flat = rotation_m.flatten()
transform_array = np.array(transform_flat, np.float32)
print(transform_array)

print("")

print("#########################################")

displacement_v = Vector3([10.0, 0.0, 0])
displacement_m = Matrix44.from_translation(displacement_v)

print("Translate Matrix")

print("")

#print(displacement_m)
transform_flat = displacement_m.flatten()
transform_array = np.array(transform_flat, np.float32)
print(transform_array)

print("")

print("#########################################")

print("Translate and Rotate")

print("")

print("")

mvMatrix_tmp = displacement_m*location_m
transform_flat = mvMatrix_tmp.flatten()
transform_array = np.array(transform_flat, np.float32)
print(transform_array)

print("")

mvMatrix = rotation_m*mvMatrix_tmp
transform_flat = mvMatrix.flatten()
transform_array = np.array(transform_flat, np.float32)
print(transform_array)

print("")

print("#########################################")

print("Rotate and Translate")

print("")

transform_flat = rotation_m.flatten()
transform_array = np.array(transform_flat, np.float32)
print(transform_array)

print("")

transform_flat = location_m.flatten()
transform_array = np.array(transform_flat, np.float32)
print(transform_array)

print("")

mvMatrix_tmp = rotation_m*location_m
transform_flat = mvMatrix_tmp.flatten()
transform_array = np.array(transform_flat, np.float32)
print(transform_array)

print("")

mvMatrix = displacement_m*mvMatrix_tmp
transform_flat = mvMatrix.flatten()
transform_array = np.array(transform_flat, np.float32)
print(transform_array)

print("")


import numpy as np

def simpson(func, z, a, b, N = 100, rel_tol = 1e-6):
    def trapecio(func, z, a, b, N = 100):
        dx = (b-a)/(N)
        f0 = func(z, a)
        fN = func(z, b)

        sum = (f0 + fN)/2
        for i in range(1,N):
            sum += func(z, a+i*dx)
        sum *= dx
        return sum

    dif = np.infty #primera vez siempre entra al bucle

    while dif > rel_tol:
        N *= 2
        sum = trapecio(func, z, a, b, N)
        sum2 = trapecio(func, z, a, b, 2*N)
        simp = (4*sum2 - sum)/3
        dif = np.abs((simp - sum)/sum)
    #se sale del ciclo porque dif < rel_tol
    return simp

def Gamma_du(z, u): #integrando de la funcion gamma con u=e^(-x)
    gdu = np.log(1/u)**(z-1)
    return gdu

def Gamma(z, dx = 1e-6):
    def pto_medio(func, z, a, b):
        dx = b-a
        med = (a+b)/2
        rec = func(z, med)
        return rec*dx
    #nuevos extremos gracias al C.V.
    a = 0
    b = 1
    g = simpson(Gamma_du, z, a + dx, b, rel_tol=dx)
    g += pto_medio(Gamma_du, z, a, a + dx)
    return g

K = 4.495 #rut 20299495-4
GAMMA_K_MEDIOS = Gamma(K/2, dx=1e-6)

def chi2(k, x):
    res = 1/(np.exp2(k/2)*GAMMA_K_MEDIOS)*x**(k/2 - 1)*np.exp(-x/2) #definicion de chi^2(x)
    return res

#calculo de int_0^a chi^2(x) dx, x<0 no es parte del dominio de chi^2
def prob_chi2(k, a, rel_tol=1e-6):
    prob = simpson(chi2, k, 0, a, rel_tol=rel_tol)
    return prob

#el problema prob_chi2(a) = 0.95 se puede reescribir como el problema del cero de una funcion
# definiendo g(a) = prob_chi2(a) - 0.95 = 0
def biseccion(func, k, a, b, tol_abs=1e-6, lim=1e6): #lim permite evitar que el programa colapse
    counter = 0
    dif = np.abs(func(k, b)-func(k, a))
    while  dif > tol_abs:
        dif = np.abs(b-a)
        p = (a+b)/2
        if counter > lim:
            print('limite de iteraciones excedido')
            return p
        fp = func(k, p)
        fa = func(k, a)
        prod = fp*fa

        if prod > 0:
            a = p
        elif prod < 0:
            b = p
        counter += 1
    print('Número de pasos con bisección:', counter)
    return p

def prob_menos_95(k, a, rel_tol=1e-10):
    res = prob_chi2(k, a, rel_tol=rel_tol) - 0.95 #se quiere encontrar res = 0
    return res

def newton(func, derivada, k, x_0, tol_abs = 1e-6):
    #f(x) = \int_0^x chi^2(t)dt - 0.95
    #f'(x) = chi^2(x)
    #x_{i+1} = x_i - f(x_i)/f'(x_i)
    dif = np.infty
    N = 0
    while dif > tol_abs:
        x_1 = x_0 - func(k, x_0)/derivada(k, x_0)
        dif = np.abs(x_1-x_0)
        x_0 = x_1
        N+=1
    print('Número de pasos con Newton:', N)
    return x_1

tol = 1e-5
a1 = biseccion(prob_menos_95, K, 0, 20, tol_abs=tol)
a2 = newton(prob_menos_95, chi2, K, 1, tol_abs=tol)
print('a por Biseccion:', a1)
print('a por Newton:', a2)
print('Diferencia:', np.abs(a1-a2))
a3 = newton(prob_menos_95, chi2, K, a1, tol_abs=1e-12)
print('a por Newton dando como punto inicial el de biseccion: ', a3)


import numpy as np
import pandas as pd

values = np.array([1, 3, 2, 4, 1, 6, 4])
example_df = pd.DataFrame({
    'value': values,
    'even': values % 2 == 0,
    'above_three': values > 3
}, index=['a', 'b', 'c', 'd', 'e', 'f', 'g'])

# Change False to True for each block of code to see what it does

# Standardize each group
if False:
    def standardize(xs):
        return (xs - xs.mean()) / xs.std()

    grouped_data = example_df.groupby('even')
    print(grouped_data['value'].apply(standardize))

# Find second largest value in each group
if False:
    def second_largest(xs):
        sorted_xs = xs.sort(inplace=False, ascending=False)
        return sorted_xs.iloc[1]

    grouped_data = example_df.groupby('even')
    print(grouped_data['value'].apply(second_largest))

# --- Quiz ---
# DataFrame with cumulative entries and exits for multiple stations
ridership_df = pd.DataFrame({
    'UNIT': ['R051', 'R079', 'R051', 'R079', 'R051', 'R079', 'R051', 'R079',
             'R051'],
    'TIMEn': ['00:00:00', '02:00:00', '04:00:00', '06:00:00', '08:00:00',
              '10:00:00', '12:00:00', '14:00:00', '16:00:00'],
    'ENTRIESn': [3144312, 8936644, 3144335, 8936658, 3144353, 8936687, 3144424,
                 8936819, 3144594],
    'EXITSn': [1088151, 13755385, 1088159, 13755393, 1088177, 13755598, 1088231,
               13756191, 1088275]
})

def diff(group):
    return group - group.shift(1)

def get_hourly_entries_and_exits(entries_and_exits):
    """
    Fill in this function to take a DataFrame with cumulative entries
    and exits and return a DataFrame with hourly entries and exits.
    The hourly entries and exits should be calculated separately for
    each station (the 'UNIT' column).

    Hint: Use the `get_hourly_entries_and_exits()` function you wrote
    in a previous quiz, DataFrame Vectorized Operations, and the `.apply()`
    function, to help solve this problem.
    """
    result = entries_and_exits.groupby('UNIT')[['ENTRIESn', 'EXITSn']].apply(diff)
    return result

print(get_hourly_entries_and_exits(ridership_df).dropna())


# encoding=utf8

"""
Module containing functions and methods for approximating the integral of functions

"""

import numpy as np

def trapezoidal_rule(f, a, b):
    r"""
    Approximates the integral of a function over the interval :math:`[a, b]` using the
    Trapezoidal Rule.

    Parameters
    ----------
    f : function
        Given function to approximate derivative at supplied value of :math:`x`. Must be
        a callable function with one parameter representing a single variable function.
    a : int or float
        The lower value of the integral to approximate
    b : int or float
        The upper lower of the integral to approximate

    Returns
    -------
    float
        The approximated value of the function integrated over the interval :math:`[a, b]`.

    Notes
    -----
    The Trapezoidal Rule gives an approximation of the integral of a function over the interval
    :math:`[a, b]`. The Trapezoidal Rule is defined as:

    .. math::

        \int^a_b f(x) dx = \frac{h}{2}[f(x_0) + f(x_1)] - \frac{h^3}{12} f^{\prime \prime} (\epsilon)

    Where :math:`- \frac{h^3}{12} f^{\prime \prime} (\epsilon)` is the error term. The Trapezoidal Rule
    approximates the integral :math:`\int^a_b f(x) dx` by using the area of a trapezoid, hence its name.

    Examples
    --------
    >>> def f(x): return x ** 4
    >>> trapezoidal_rule(f, 0.5, 1)
    .265625
    >>> def f2(x): return 2 / (x - 4)
    >>> trapezoidal_rule(f2, 0, 0.5)
    -.2678571

    References
    ----------
    Burden, R. L., & Faires, J. D. (2011). Numerical analysis (9th ed.).
        Boston, MA: Brooks/Cole, Cengage Learning.

    """
    if callable(f) is False:
        raise TypeError('f must be a function with one parameter (variable)')
    if isinstance(a, float) is False:
        a = float(a)
    if isinstance(b, float) is False:
        b = float(b)

    h = b - a

    approx = (h / 2.) * (f(a) + f(b))

    return approx

def composite_trapezoidal(f, a, b, n=6):
    if callable(f) is False:
        raise TypeError('f must be a function with one parameter (variable)')

    h = (b - a) / n

    xj = np.linspace(a, b, n + 1)

    return (h / 2) * (f(a) + 2 * np.sum(f(xj[1:-1])) + f(b))

def simpsons_rule(f, a, b):
    r"""
    Approximates the integral of a function over the interval :math:`[a, b]` using Simpson's
    Rule.

    Parameters
    ----------
    f : function
        Given function to approximate derivative at supplied value of :math:`x`. Must be
        a callable function with one parameter representing a single variable function.
    a : int or float
        The lower value of the integral to approximate
    b : int or float
        The upper lower of the integral to approximate

    Returns
    -------
    float
        The approximated value of the function integrated over the interval :math:`[a, b]`.

    Notes
    -----
    Simpson's rule is another method in numerical analysis for approximating the definite
    integral of a function. The rule is defined as:

    .. math::

        \int_{x_0}^{x_2} f(x) dx = \frac{h}{3}[f(x_0) + 4f(x_1) + f(x_2)] - \frac{h^5}{90}f^{(4)} (\epsilon)

    Where :math:`\frac{h^5}{90}f^{(4)} (\epsilon)` is the error term.

    Examples
    --------
    >>> def f(x): return x ** 4
    >>> simpsons_rule(f, 0.5, 1)
    .1940104
    >>> def f2(x): return 2 / (x - 4)
    >>> simpsons_rule(f2, 0, 0.5)
    -.2670635

    References
    ----------
    Burden, R. L., & Faires, J. D. (2011). Numerical analysis (9th ed.).
        Boston, MA: Brooks/Cole, Cengage Learning.

    """
    if callable(f) is False:
        raise TypeError('f must be a function with one parameter (variable)')

    if isinstance(a, float) is False:
        a = float(a)
    if isinstance(b, float) is False:
        b = float(b)

    h = (b - a) / 2.
    x0, x1, x2 = a, a + h, b

    approx = (h / 3.) * (f(x0) + 4. * f(x1) + f(x2))

    return approx

def composite_simpsons_rule(f, a, b, n=6):
    if callable(f) is False:
        raise TypeError('f must be a function with one parameter (variable)')

    h = (b - a) / n

    xj = np.linspace(a, b, n + 1)[1:-1]

    return (h / 3) * (f(a) + 2 * np.sum(f(xj[1::2])) + 4 * np.sum(f(xj[0::2])) + f(b))


import numpy as np
import matplotlib.pyplot as plt
from typing import List, Tuple, Callable, Union

Vector = List[float]
Function = Callable[[float], float]
Function2d = Callable[[float, float], float]

def newton_cotes(y: Vector=None, x: Vector=None, formula: str='trapz', h: float=1,
                 f: Function=None, a: float=None, b: float=None, N: int=100) -> float:
    """
    Newton-Cotes rules
    ==================
    In numerical analysis, the Newton-Cotes formulas, also called the Newton-Cotes quadrature
    rules or simply Newton-Cotes rules, are a group of formulas for numerical integration
    (also called quadrature) based on evaluating the integrand at equally spaced points.
    They are named after Isaac Newton and Roger Cotes.

    Newton-Cotes formulas can be useful if the value of the integrand at equally spaced points is given.
    If it is possible to change the points at which the integrand is evaluated,
    then other methods such as Gaussian quadrature and Clenshaw-Curtis quadrature are probably more suitable.
    `n` is the grade of the polynomial interpolation, also nodes - 1

    Parameters
    ----------
    y : Vector
        Input array to integrate
    x : Vector, optional
        The sample points corresponding to the y values. If x is None,
        the sample points are assumed to be evenly spaced h apart, by default None
    formula : str, optional
        This defines which Newton-Cotes rule apply
            trapz: trapezoidal rule, `n=1`
            simpson: Simpson's rule, `n=2`
            simpson3_8: Simpson's rule 3/8, `n=3`
            boole : Boole's rule, `n=4`
            weddle: Weddle's rule, `n=6`

            -> For simplicity is recommended to use the functions `trapz`, `simpson` etc.

    h : float, optional
        The spacing between sample points when x is None, by default 1
    f : function, optional
        funcion to integrate, if you don't define y/x
    a : float, optional
        Lower limit of the integral
    b : float, optional
        Upper limit of the integral
    N : int, optional
        Number of intervals, must be divisible by n, by default 100

    Returns
    -------
    float
        Definite integral

    Examples
    --------
    >>> N, a, b = 12, 1, 4
    >>> x = np.linspace(a, b, N+1)
    >>> y = np.sin(x)
    >>> print(newton_cotes(y,x,'simpson'))
    1.193972031074762
    >>> def fun(x): return np.sin(x)
    >>> print(newton_cotes(f=fun, a=a, b=b, N=N, formula='simpson3_8'))
    1.1940051049177476

    """
    if   formula == 'trapz':
        n, fs = 1, '(y[i] + y[i+1]) * d[i]/2'
    elif formula == 'simpson':
        n, fs = 2, '(y[n*i] + 4*y[n*i + 1] + y[n*i + 2]) * d[i]/3'
    elif formula == 'simpson3_8':
        n, fs = 3, '(y[n*i] + 3*y[n*i + 1] + 3*y[n*i + 2] + y[n*i + 3]) * 3*d[i]/8'
    elif formula == 'boole':
        n, fs = 4, '(7*y[n*i] + 32*y[n*i + 1] + 12*y[n*i + 2] + 32*y[n*i + 3] + 7*y[n*i + 4]) * 2*d[i]/45'
    elif formula == 'weddle':
        n, fs = 6, '(y[n*i] + 5*y[n*i + 1] + y[n*i + 2] + 6*y[n*i + 3] + y[n*i + 4] + 5*y[n*i + 5] + y[n*i + 6]) * 3*d[i]/10'
    else: raise ValueError('Wrong formula')

    if y is None and x is None:
        if not all(p is not None for p in [f,a,b]): raise ValueError(f"If you don't define y/x you must define f,a and b")
        if N%n != 0: raise ValueError(f'{N=} must be divisible by {n=}')
        x = np.linspace(a,b,N+1)
        y = f(x)

    if (len(y)-1)%n != 0: raise ValueError(f'The length of the array y={len(y)} - 1 must be multiple of {n=} in order to divide the interval')
    nh = int((len(y)-1)/n)

    if x is None:
        d = np.array([h]*nh)
        # x is only for checking
        x = np.empty(len(y))
    else: d = np.diff(x)[::n]

    if len(y) != len(x): raise ValueError(f'The length of x={len(x)} and y={len(y)} must be the same')

    s = 0
    for i in range(nh):
        s += eval(fs)
    return s

def trapz(y: Vector=None, x: Vector=None, h: float=1,
          f: Function=None, a: float=None, b: float=None, N: int=100) -> float:
    """
    Trapezoidal Rule
    ================
    The trapezoidal rule (also known as the trapezoid rule or trapezium rule)
    is a technique for approximating the definite integral.
    The trapezoidal rule works by approximating the region under the graph of the function
    `f(x)` as a trapezoid and calculating its area.
    `n=1`, where n is the grade of the polynomial interpolation, also nodes - 1.
    This is a special case of Newton-Cotes formulas

    Parameters
    ----------
    y : Vector
        Input array to integrate
    x : Vector, optional
        The sample points corresponding to the y values. If x is None,
        the sample points are assumed to be evenly spaced h apart, by default None
    h : float, optional
        The spacing between sample points when x is None, by default 1
    f : function, optional
        funcion to integrate, if you don't define y/x
    a : float, optional
        Lower limit of the integral
    b : float, optional
        Upper limit of the integral
    N : int, optional
        Number of intervals, must be divisible by n, by default 100

    Returns
    -------
    float
        Definite integral

    Examples
    --------
    >>> N, a, b = 12, 1, 4
    >>> x = np.linspace(a, b, N+1)
    >>> y = np.sin(x)
    >>> print(trapz(y,x))
    1.187720971137812
    >>> def fun(x): return np.sin(x)
    >>> print(trapz(f=fun, a=a, b=b, N=N))
    1.187720971137812

    """
    return newton_cotes(y, x, 'trapz', h, f, a, b, N)

def simpson(y: Vector=None, x: Vector=None, h: float=1,
            f: Function=None, a: float=None, b: float=None, N: int=100) -> float:
    """
    Simpson's Rule
    ============
    In numerical integration, Simpson's rules are several approximations for definite integrals,
    named after Thomas Simpson (1710-1761).
    This is the  most basic of these rules, called Simpson's 1/3 rule, or just Simpson's rule.
    `n=2`, where n is the grade of the polynomial interpolation, also nodes - 1.
    This is a special case of Newton-Cotes formulas

    Parameters
    ----------
    y : Vector
        Input array to integrate
    x : Vector, optional
        The sample points corresponding to the y values. If x is None,
        the sample points are assumed to be evenly spaced h apart, by default None
    h : float, optional
        The spacing between sample points when x is None, by default 1
    f : function, optional
        funcion to integrate, if you don't define y/x
    a : float, optional
        Lower limit of the integral
    b : float, optional
        Upper limit of the integral
    N : int, optional
        Number of intervals, must be divisible by n, by default 100

    Returns
    -------
    float
        Definite integral

    Examples
    --------
    >>> N, a, b = 12, 1, 4
    >>> x = np.linspace(a, b, N+1)
    >>> y = np.sin(x)
    >>> print(simpson(y,x))
    1.193972031074762
    >>> def fun(x): return np.sin(x)
    >>> print(simpson(f=fun, a=a, b=b, N=N))
    1.193972031074762

    """
    return newton_cotes(y, x, 'simpson', h, f, a, b, N)

def simpson3_8(y: Vector=None, x: Vector=None, h: float=1,
               f: Function=None, a: float=None, b: float=None, N: int=99) -> float:
    """
    Simpson's Rule 3/8
    ================
    In numerical integration, Simpson's rules are several approximations for definite integrals,
    named after Thomas Simpson (1710-1761).
    This is the second of these rules, called Simpson's 3/8 rule or Simpson's second rule.
    `n=3`, where n is the grade of the polynomial interpolation, also nodes - 1.
    This is a special case of Newton-Cotes formulas

    Parameters
    ----------
    y : Vector
        Input array to integrate
    x : Vector, optional
        The sample points corresponding to the y values. If x is None,
        the sample points are assumed to be evenly spaced h apart, by default None
    h : float, optional
        The spacing between sample points when x is None, by default 1
    f : function, optional
        funcion to integrate, if you don't define y/x
    a : float, optional
        Lower limit of the integral
    b : float, optional
        Upper limit of the integral
    N : int, optional
        Number of intervals, must be divisible by n, by default 99

    Returns
    -------
    float
        Definite integral

    Examples
    --------
    >>> N, a, b = 12, 1, 4
    >>> x = np.linspace(a, b, N+1)
    >>> y = np.sin(x)
    >>> print(simpson3_8(y,x))
    1.1940051049177476
    >>> def fun(x): return np.sin(x)
    >>> print(simpson3_8(f=fun, a=a, b=b, N=N))
    1.1940051049177476

    """
    return newton_cotes(y, x, 'simpson3_8', h, f, a, b, N)

def boole(y: Vector=None, x: Vector=None, h: float=1,
          f: Function=None, a: float=None, b: float=None, N: int=100) -> float:
    """
    Boole's Rule
    ============
    In mathematics, Boole's rule, named after George Boole, is a method of numerical integration.

    `n=4`, where n is the grade of the polynomial interpolation, also nodes - 1
    This is a special case of Newton-Cotes formulas

    Parameters
    ----------
    y : Vector
        Input array to integrate
    x : Vector, optional
        The sample points corresponding to the y values. If x is None,
        the sample points are assumed to be evenly spaced h apart, by default None
    h : float, optional
        The spacing between sample points when x is None, by default 1
    f : function, optional
        funcion to integrate, if you don't define y/x
    a : float, optional
        Lower limit of the integral
    b : float, optional
        Upper limit of the integral
    N : int, optional
        Number of intervals, must be divisible by n, by default 100

    Returns
    -------
    float
        Definite integral

    """
    return newton_cotes(y, x, 'boole', h, f, a, b, N)

def weddle(y: Vector=None, x: Vector=None, h: float=1,
           f: Function=None, a: float=None, b: float=None, N: int=96) -> float:
    """
    Weddle's Rule
    ==========

    n=6, where n is the grade of the polynomial interpolation, also nodes - 1

    Parameters
    ----------
    y : Vector
        Input array to integrate
    x : Vector, optional
        The sample points corresponding to the y values. If x is None,
        the sample points are assumed to be evenly spaced h apart, by default None
    h : float, optional
        The spacing between sample points when x is None, by default 1
    f : function, optional
        funcion to integrate, if you don't define y/x
    a : float, optional
        Lower limit of the integral
    b : float, optional
        Upper limit of the integral
    N : int, optional
        Number of intervals, must be divisible by n, by default 96

    Returns
    -------
    float
        Definite integral

    """
    return newton_cotes(y, x, 'weddle', h, f, a, b, N)

def newton_cotes2(y: Vector=None, x: Vector=None, h: float=1,
                 f: Function=None, a: float=None, b: float=None, n: int=100) -> float:
    n = len(y)-1
    if   n == 1: return trapz(y,x,h)
    elif n == 2: return simpson(y,x,h)
    elif n == 3: return simpson3_8(y,x,h)
    elif n == 4: return boole(y,x,h)
    elif n == 6: return weddle(y,x,h)

def odeEuler(f: Function2d, t0: float, tfin: float, N: int, y0: Union[float, Vector]) -> Tuple[Vector, Vector]:
    """
    Euler Method
    ============

    Parameters
    ----------
    f : Function2d
        Right-hand side of the differential equation `y' = f(t,y), y(t0) = y0`
    t0 : float
        Initial time
    tfin : float
        Final time
    N : int
        Number of partitions
    y0 : float
        Initial value

    Returns
    -------
    Tuple[Vector, Vector]
        The time and value vectors of the solution

    Examples
    --------
    >>> def q(t): return 1/2 + 1/2 * np.cos(t**2)
    >>> def f(t, y): return q(t) - y
    >>> T, U = odeEuler(f, 0, 6, 1000, 0)
    >>> import matplotlib.pyplot as plt
    >>> plt.plot(T,U)
    >>> plt.show()
    plot figure
    >>> T, U = odeEuler(f, 0, 6, 1000, [0,1])
    >>> for i in range(len(T)):
            plt.plot(T[i], U[i])
    >>> plt.show()
    plot figure

    """
    if isinstance(y0, list) or type(y0) == np.ndarray:
        leny0 = len(y0)

        T = np.tile(np.linspace(t0, tfin, N+1), (leny0,1))
        h = (tfin - t0) / N

        U = np.empty([leny0, N+1])
        U[:,0] = y0
        for i in range(1, N+1):
            U[:,i] = U[:,i-1] + h*f(T[:,i-1], U[:,i-1])

    else:
        T = np.linspace(t0, tfin, N+1)
        h = (tfin - t0) / N
        U = np.empty(N+1)
        U[0] = y0
        for i in range(1, N+1):
            U[i] = U[i-1] + h*f(T[i-1], U[i-1])

    return T, U

def slope_field(f: Function2d, range: list = None,
                xlim: list = None, ylim: list = None,
                normalize: bool = True, plot_type: str = 'quiver',
                density: int = 20, color: bool = True,
                show: bool = True, cmap: str = 'viridis'):
    """
    Slope Field
    ===========
    Graphical representation of the solutions
    to a first-order differential equation `y' = f(t,y)`

    Parameters
    ----------
    f : Function2d
        Function with 2 parameters `y' = f(t,y)`

    range : list, optional
        Sets both limits x/y of the plot, by default [-5, 5]

    xlim : list, optional
        Sets the x limits of the plot, if `range` is defined, xlim is already set, by default [-5, 5]

    ylim : list, optional
        Sets the y limits of the plot, if `range` is defined, ylim is already set, by default [-5, 5]

    normalize : bool, optional
        Normalize the slope field, by default True

    plot_type : str, optional
        Defines the plot type
            quiver: -> plt.quiver()
            streamplot: -> plt.streamplot()
        by default 'quiver'

    density : int, optional
        Density of arrows, by default 20

    color : bool, optional
        Color of the arrows, by default True

    show : bool, optional
        Shows the plot, by default True

    cmap : str, optional
        https://matplotlib.org/stable/tutorials/colors/colormaps.html

    Examples
    --------
    >>> def fun(x,y): return x + np.sin(y)
    >>> slope_field(fun, range=[-2,2], plot_type='streamplot', cmap='plasma')
    plot figure
    >>> slope_field(fun, xlim=[-3,2], ylim=[-1,1], color=False, normalize=False, density=30, show=False)
    >>> T, U = odeEuler(fun, -3, 2, 1000, 0.1)
    >>> import matplotlib.pyplot as plt
    >>> plt.plot(T,U)
    >>> plt.show()
    plot figure

    """

    if range is None and xlim is None and ylim is None:
        range = [-5,5]
        x1, x2 = range
        y1, y2 = range

    elif xlim is None and ylim is None:
        x1, x2 = range
        y1, y2 = range

    elif range is None:
        x1, x2 = xlim
        y1, y2 = ylim

    else:
        raise ValueError('Must speciefy either range or xlim/ylim')

    x = np.linspace(x1, x2, density)
    y = np.linspace(y1, y2, density)

    X, Y = np.meshgrid(x, y)

    dx, dy = np.ones(X.shape), f(X,Y)
    if normalize:
        norm = np.sqrt(dx**2 + dy**2)
        dx, dy = dx/norm , dy/norm

    if plot_type == 'quiver':
        #color = np.sqrt(((dx+4)/2)*2 + ((dy+4)/2)*2)
        if color: plt.quiver(X, Y, dx, dy, dy, cmap=cmap)
        else: plt.quiver(X, Y, dx, dy)

    elif plot_type == 'streamplot':
        if color: plt.streamplot(X, Y, dx, dy, color=dy, cmap=cmap)
        else: plt.streamplot(X, Y, dx, dy, color='k')

    else:
        raise ValueError("It only accepts either 'quiver' or 'streamplot'")

    plt.title(f'Slope Field ({plot_type})')
    plt.xlabel('x')
    plt.ylabel('y')
    if show: plt.show()


import numpy as np

# Creating vectors (unidimensional arrays)
vector_a = np.arange(5) # 0 to 4 array
vector_b = np.arange(start=3, stop=7, step=1) # start, stop, step
vector_c = np.arange(11, 1, -2)
vector_d = np.linspace(start=0, stop=1, num=11) # Automatic step calculation with explicit array length definition (third parameter)

print(f'\nVector A:\n', vector_a)
print(f'\nVector B:\n', vector_b)
print(f'\nVector C:\n', vector_c)
print(f'\nVector D:\n', vector_d)

# Creating matrix (bidimensional arrays)
matrix_a = np.arange(9).reshape(3, 3) # Array of 3 arrays each one with 3 ascendent numbers from 0 to reach 8

# for index, array in enumerate(matrix_a):
#     print(f'{index} array:', array)
#     for element in array:
#         print(element)
print(f'\nMatrix:\n', matrix_a)

# Creating tensors (+3 dimensional arrays)
tensor_a = np.arange(12).reshape(3, 2, 2)

print(f'\nTensor:\n', tensor_a)

# Creating an array from a list
my_list = [1, 2, 3, 4, 5]

vector_from_list_a = np.array(my_list)
vector_from_list_b = np.array([1, 2, 3, 4, 5])
print(f'\nVector from list A:\n', vector_from_list_a)
print(f'\nVector from list B:\n', vector_from_list_b)

matrix_from_list = np.array([[1, 2, 3], [4, 5, 6]])
print(f'\nMatrix from list:\n', matrix_from_list)

tensor_from_list = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]], [[9, 10], [11, 12]]])
print(f'\nTensor from list:\n', tensor_from_list)

# Creating empty arrays
empty_array_a = np.zeros(5)
empty_array_b = np.zeros((2, 2))
empty_array_c = np.ones((2, 2))

print(f'\nEmpty array A:\n{empty_array_a}')
print(f'\nEmpty array B:\n{empty_array_b}')
print(f'\nEmpty array C:\n{empty_array_c}')

# Filled arrays
filled_array_a = np.full(shape=(2, 2), fill_value=7)
filled_array_b = np.full(shape=(2, 2), fill_value='a')

print(f'\nFilled array A:\n{filled_array_a}')
print(f'\nFilled array B:\n{filled_array_b}')

# Reusing the structure of an array to construct another array
base = np.linspace(2, 6, 4)
reuse_array_a = np.full_like(base, np.pi)

print(f'\nReusing an array:\nBase: {base}\nTo: {reuse_array_a}\n')