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)


# 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')


import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from scipy.special import expit
import os

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'

# Choosing activation functions for multilayer neural networks

# Logistic function recap

X = np.array([1, 1.4, 2.5]) ## first value must be 1
w = np.array([0.4, 0.3, 0.5])

def net_input(X, w):
    return np.dot(X, w)

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

def logistic_activation(X, w):
    z = net_input(X, w)
    return logistic(z)

print('P(y=1|x) = %.3f' % logistic_activation(X, w))

# W : array with shape = (n_output_units, n_hidden_units+1)
# note that the first column are the bias units

W = np.array([[1.1, 1.2, 0.8, 0.4],
              [0.2, 0.4, 1.0, 0.2],
              [0.6, 1.5, 1.2, 0.7]])

# A : data array with shape = (n_hidden_units + 1, n_samples)
# note that the first column of this array must be 1

A = np.array([[1, 0.1, 0.4, 0.6]])
Z = np.dot(W, A[0])
y_probas = logistic(Z)
print('Net Input: \n', Z)

print('Output Units:\n', y_probas)

y_class = np.argmax(Z, axis=0)
print('Predicted class label: %d' % y_class)

# Estimating class probabilities in multiclass classification via the softmax function

def softmax(z):
    return np.exp(z) / np.sum(np.exp(z))

y_probas = softmax(Z)
print('Probabilities:\n', y_probas)

np.sum(y_probas)

Z_tensor = tf.expand_dims(Z, axis=0)
tf.keras.activations.softmax(Z_tensor)

# Broadening the output spectrum using a hyperbolic tangent
def tanh(z):
    e_p = np.exp(z)
    e_m = np.exp(-z)
    return (e_p - e_m) / (e_p + e_m)

z = np.arange(-5, 5, 0.005)
log_act = logistic(z)
tanh_act = tanh(z)
plt.ylim([-1.5, 1.5])
plt.xlabel('Net input $z$')
plt.ylabel('Activation $\phi(z)$')
plt.axhline(1, color='black', linestyle=':')
plt.axhline(0.5, color='black', linestyle=':')
plt.axhline(0, color='black', linestyle=':')
plt.axhline(-0.5, color='black', linestyle=':')
plt.axhline(-1, color='black', linestyle=':')
plt.plot(z, tanh_act,
    linewidth=3, linestyle='--',
    label='Tanh')
plt.plot(z, log_act,
    linewidth=3,
    label='Logistic')
plt.legend(loc='lower right')
plt.tight_layout()
plt.show()

np.tanh(z)
tf.keras.activations.tanh(z)

expit(z)

tf.keras.activations.sigmoid(z)

# Rectified linear unit activation

tf.keras.activations.relu(z)

Chapter 13/04 Choosing activation functions for multilayer neural networks/program.py


import numpy as np

# We will add the vector v to each row of the matrix x,
# storing the result in the matrix y
x = np.array([[1,2,3], [4,5,6], [7,8,9], [10, 11, 12]])
print(x)
v = np.array([1, 0, 1])
print(v)
y = np.empty_like(x)   # Create an empty matrix with the same shape as x
print(y)

# Add the vector v to each row of the matrix x with an explicit loop
for i in range(4):
    y[i, :] = x[i, :] + v

# Now y is the following
# [[ 2  2  4]
#  [ 5  5  7]
#  [ 8  8 10]
#  [11 11 13]]
print(y)

vv = np.tile(v, (4, 1))   # Stack 4 copies of v on top of each other
print(vv)                 # Prints "[[1 0 1]
                          #          [1 0 1]
                          #          [1 0 1]
                          #          [1 0 1]]"
yy = x + vv  # Add x and vv elementwise
print(yy)  # Prints "[[ 2  2  4
          #          [ 5  5  7]
          #          [ 8  8 10]
          #          [11 11 13]]"

yyy = x + v  # Add v to each row of x using broadcasting
print(y)  # Prints "[[ 2  2  4]
          #          [ 5  5  7]
          #          [ 8  8 10]
          #          [11 11 13]]"


import numpy as np
from numpy.core._multiarray_umath import ndarray

def Jacobi_nonmatrix(A, b):
    n = len(A)
    x = np.zeros(n)
    y = np.ones(n)
    N = 100
    tmp = np.zeros(n)
    #    for k in range(N):
    while max(abs((y - tmp))) > 0.001:
        for i in range(0, n):
            s1 = sum(A[i][t] * x[t] for t in range(i))
            s2 = sum(A[i][t] * x[t] for t in range(i + 1, n))
            y[i] = (b[i] - s1 - s2) / A[i][i]
        tmp = x.copy()
        x = y.copy()  # You can understand x and y as pointers to a specific array.
        # Direct assignment will cause x and y to point to the same object, causing the x and y values to change
        # together during the iteration.
    return y

def Jacobi_matrix(A, b):
    # Use numpy matrix operation, save time
    x = np.zeros(len(b))
    D = np.diagflat(np.diag(A))
    LU = A - D
    N = 100
    for k in range(N):
        x = np.dot(np.dot(-np.linalg.inv(D), LU), x) + np.dot(np.linalg.inv(D), b)
    #    D = np.diag(A)
    #    LU = A - np.diagflat(D)
    #    N = 100
    #    for k in range(N):
    #       x = (b - np.dot(LU, x))/D
    return x

def Gauss_seidel(A, b):
    n = len(A)
    x = np.zeros(n)
    tmp = np.ones(n)
    #   N = 100
    #   for k in range(N):
    while max(abs((x - tmp))) > 0.001:
        tmp = x.copy()
        for i in range(n):
            x[i] = b[i] / A[i][i] - sum((A[i][j] / A[i][i]) * x[j] for j in range(i)) - sum(
                (A[i][j] / A[i][i]) * x[j] for j in range(i + 1, n))
    return x

# Test case 1, problem 15, problem 17

A = [[5., 2., 1.], [-1., 4., 2.], [2., -5., 10.]]
A = np.array(A)
b = [-12., 10., 1.]
b = np.array(b)
print(Jacobi_nonmatrix(A, b))
print(Jacobi_matrix(A, b))
print(Gauss_seidel(A, b))


import numpy as np
from numpy.random import rand
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from timeit import timeit as ti

#problem 1
def arrmul(A,B):
    new = []
    for i in range(len(A)):
        newrow = []
        for k in range(len(B[0])):
            tot = 0
            for j in range(len(B)):
                tot += A[i][j] * B[j][k]
            newrow.append(tot)
        new.append(newrow)
    return new

def prob1():
    k = 200
    A = [range(i, i+k) for i in range(0, k**2, k)]
    number = 5
    print "problem 1"
    tm = ti("arrmul(A,A)", setup="from __main__ import A, arrmul", number=number)
    print "arrmul(A,A) executed ", number, " times in ", tm, " seconds."
    A = np.array(A)
    number = 5
    tm = ti("np.dot(A,A)", setup="import numpy as np; from __main__ import A", number=number)
    print "np.dot(A,A) executed ", number, " times in ", tm, " seconds."

    #problem 2
def prob2():
    print "problem 2"
    A = rand(1000,1000)
    B = np.empty_like(A)
    for i in xrange(100):
            B[:] = rand(1000,1000)
            A[A<B] = B[A<B]
    np.exp(A, out=A)
    print np.average(np.max(A, axis=1))

def prob3():
    #problem 3
    print "problem 3"
    A = rand(1000,1000)
    number = 100
    tm = ti("A.reshape(A.size)", setup="from __main__ import A", number=number)
    print "A.reshape(A.size) executed ", number, " times in ", tm, " seconds."
    number = 100
    tm = ti("A.flatten()", setup="from __main__ import A", number=number)
    print "A.flatten() executed ", number, " times in ", tm, " seconds."
    number = ti("A.reshape((1,A.size))", setup="from __main__ import A", number=number)
    print "A.reshape((1,A.size)) executed ", number, " times in ", tm, " seconds."

    #part 2 of problem 3
    print "the difference is that np.vstack(A) returns a new array"
    print "while A.T returns a view"
    print "A.T is much faster"
    A = rand(1,1000000)
    number = 500
    tm = ti("np.vstack(A)", setup="import numpy as np; from __main__ import A", number=number)
    print "np.vstack(A) executed ", number, " times in ", tm, " seconds."
    number = 500
    tm = ti("A.T", setup="from __main__ import A", number=number)
    print "A.T executed ", number, " times in ", tm, " seconds."

def laplace(U,tol):
    new = U.copy()
    dif = tol
    while tol<= dif:
        new[1:-1,1:-1] = (U[:-2,1:-1] + U[2:,1:-1] + U[1:-1,:-2] + U[1:-1,2:]) / 4.
        dif = np.max(np.absolute(U-new))
        U[:] = new

def prob4():
    n = 100
    tol=.0001
    U=np.ones((n,n))
    U[:,0] = 100
    U[:,-1] = 100
    U[0] = 0
    U[-1] = 0
    laplace(U, tol)
    fig = plt.figure()
    ax = fig.gca(projection='3d')
    X = np.linspace(0,1,n)
    Y = np.linspace(0,1,n)
    X, Y = np.meshgrid(X, Y)
    ax.plot_surface(X, Y, U, rstride=5)
    plt.show()

#problem 5

def broadcast_1():
    """All input arrays have exactly the same shape"""
    a = np.random.rand(4, 5)
    b = np.random.rand(4, 5)
    r = a * b
    print "Case 1: {} * {} = {}".format(a.shape, b.shape, r.shape)

def broadcast_2():
    """All input arrays are of the same dimension and
    the length of corresponding dimensions match or is 1"""

    a = np.random.rand(5, 4, 1, 6)
    b = np.random.rand(5, 4, 1, 1)
    r = a * b
    print "Case 2: {} * {} = {}".format(a.shape, b.shape, r.shape)

def broadcast_3():
    """All input arrays of fewer dimension can have 1
    prepended to their shapes to satisfy the second criteria."""

    a = np.random.rand(1, 6)
    b = np.random.rand(5, 4, 1, 6)
    r = a * b
    print "Case 3: {} * {} = {}".format(a.shape, b.shape, r.shape)

def prob_broadcasting():
    #problem 5
    broadcast_1()
    broadcast_2()
    broadcast_3()


#!/usr/bin/env python
# coding: utf-8
# ## Monte Carlo - Euler Discretization - Part II
# *Suggested Answers follow (usually there are multiple ways to solve a problem in Python).*
# *Euler Discretization – continued.*
# In[ ]:
import numpy as np
import pandas as pd
from pandas_datareader import data as web
from scipy.stats import norm
import matplotlib.pyplot as plt
get_ipython().run_line_magic('matplotlib', 'inline')
data = pd.read_csv('D:/Python/MSFT_2000.csv', index_col = 'Date')
log_returns = np.log(1 + data.pct_change())
stdev = log_returns.std() * 250 ** 0.5
stdev = stdev.values
r = 0.025
T = 1.0
t_intervals = 250
delta_t = T / t_intervals
iterations = 10000
Z = np.random.standard_normal((t_intervals + 1, iterations))
S = np.zeros_like(Z)
S0 = data.iloc[-1]
S[0] = S0
for t in xrange(1, t_intervals + 1):
    S[t] = S[t-1] * np.exp((r - 0.5 * stdev ** 2) * delta_t + stdev * delta_t ** 0.5 * Z[t])
plt.figure(figsize=(10, 6))
plt.plot(S[:, :10]);
# ******
# Use numpy.maximum to create a vector with as many elements as there are columns in the S matrix.
# In[ ]:
p = np.maximum(S[-1] - 110, 0)
# In[ ]:
p
# In[ ]:
p.shape
# Use the following formula to forecast the price of a stock option.
# $$
# C = \frac{exp(-r \cdot T) \cdot \sum{p_i}}{iterations}
# $$
# In[ ]:
np.sum(p)
# In[ ]:
C = np.exp(-r * T) * np.sum(p) / iterations
C
# Because this pricing model is based on random iterations, you will obtain a different result every time you re-run the code in this document. Expand the “Kernel” list from the Jupyter menu and click on “Restart and run all”/”Restart & run all cells” to verify this is true.


# solutions.py
"""Volume 2 Lab 14: Optimization Packages II (CVXOPT)"""

from cvxopt import matrix, solvers
import numpy as np
from scipy import linalg as la

def prob1():
    """Solve the following convex optimization problem:

    minimize        2x + y + 3z
    subject to      x + 2y          >= 3
                    2x + 10y + 3z   >= 10
                    x               >= 0
                    y               >= 0
                    z               >= 0

    Returns (in order):
        The optimizer x (ndarray)
        The optimal value (sol['primal objective'])
    """

    # Note that 'matrix' initializes by column, not row.
    c = matrix([2., 1., 3.])
    G = matrix(np.array([[-1.,-2.,0.],
                         [-2.,-10.,-3.],
                         [-1.,0.,0.],
                         [0.,-1.,0.],
                         [0.,0.,-1.]]))

    h = matrix([ -3., -10., 0., 0., 0.])
    sol = solvers.lp(c,G,h)
    return np.ravel(sol['x']), sol['primal objective']

    # Answers:
    # np.array([ -1.14760916e-09,   1.50000000e+00,   6.95278285e-11])
    # 1.4999999996249482

# Problem 2
def l1Min(A, b):
    """Calculate the solution to the optimization problem

        minimize    ||x||_1
        subject to  Ax = b

    Parameters:
        A ((m,n) ndarray)
        b ((m, ) ndarray)

    Returns:
        The optimizer x (ndarray), without any slack variables u
        The optimal value (sol['primal objective'])
    """

    assert A.shape[0] == b.shape[0], "mismatched dimensions"

    n = A.shape[1]
    I = np.eye(n, dtype=np.float)

    '''The optimization problem to solve is:

    minimize                [u]
                      [1 0] [x]

    subject to      [-I  I] [u]     [0]
                    [-I -I] [x]  <= [0]

                            [u]
                    [0   A] [x]  ==  b
    '''

    # Build the matrices for cvxopt (make sure dtype=np.floats)
    c = matrix(np.hstack((np.ones(n), np.zeros(n))).astype(np.float))
    G = matrix(np.vstack((np.hstack((-I, I)),np.hstack((-I, -I)))))
    h = matrix(np.zeros(2*n))
    new_A = matrix(np.hstack((np.zeros_like(A), A)).astype(np.float))
    new_b = matrix(b.astype(np.float))

    # Perform the optimization.
    sol = solvers.lp(c, G, h, new_A, new_b)

    # Flatten out the array and remove the u values.
    return np.ravel(sol['x'])[n:], sol['primal objective']

def prob3():
    """Solve the transportation problem by converting the last equality constraint
    into inequality constraints.

    Returns (in order):
        The optimizer x (ndarray)
        The optimal value (sol['primal objective'])
    """
    c = matrix([4., 7., 6., 8., 8., 9.])
    G = matrix(np.array([[-1.,0.,0.,0.,0.,0.],
                         [0.,-1.,0.,0.,0.,0.],
                         [0.,0.,-1.,0.,0.,0.],
                         [0.,0.,0.,-1.,0.,0.],
                         [0.,0.,0.,0.,-1.,0.],
                         [0.,0.,0.,0.,0.,-1.],
                         [0.,1.,0.,1.,0.,1.],
                         [0.,-1.,0.,-1.,0.,-1.]]))
    h = matrix([0.,0.,0.,0.,0.,0., 8., -8.])
    A = matrix(np.array([[1.,1.,0.,0.,0.,0.],
                         [0.,0.,1.,1.,0.,0.],
                         [0.,0.,0.,0.,1.,1.],
                         [1.,0.,1.,0.,1.,0.]]))
    b = matrix([7.,2.,4.,5.])
    sol = solvers.lp(c,G,h,A,b)
    return np.ravel(sol['x']), sol['primal objective']

    # Answers:
    # np.array([[ 5.00e+00],[ 2.00e+00],[ -7.03e-09],
    #           [ 2.00e+00],[ -5.45e-09],[ 4.00e+00]])
    # 86

def prob4():
    """Find the minimizer and minimum of

    g(x,y,z) = (3/2)x^2 + 2xy + xz + 2y^2 + 2yz + (3/2)z^2 + 3x + z

    Returns (in order):
        The optimizer x (ndarray)
        The optimal value (sol['primal objective'])
    """
    P = matrix(np.array([[3.,2.,1.],
                         [2.,4.,2.],
                         [1.,2.,3.]]))

    q = matrix([3., 0., 1.])
    sol = solvers.qp(P, q)
    return np.ravel(sol['x']), sol['primal objective']

    # Answers:
    # np.array([[-1.50],[ 1.00],[-.5]])
    # -2.5

# Problem 5
def l2Min(A, b):
    """Calculate the solution to the optimization problem

        minimize    ||x||_2
        subject to  Ax = b

    Parameters:
        A ((m,n) ndarray)
        b ((m, ) ndarray)

    Returns:
        The optimizer x (ndarray)
        The optimal value (sol['primal objective'])
    """

    assert A.shape[0] == b.shape[0], "mismatched dimensions"

    n = A.shape[1]
    I = np.eye(n, dtype=np.float)

    '''The optimization problem to solve is:

        minimize              x*x
        subject to          A  =  b
    '''

    # Build the matrices for cvxopt (make sure dtype=np.floats)
    P = matrix(2*I)
    q = matrix(np.zeros(n))
    new_A = matrix(A.astype(np.float))
    new_b = matrix(b.astype(np.float))

    # Perform the optimization.
    sol = solvers.qp(P, q, A=new_A, b=new_b)

    # Flatten out the array and only get the x value.
    return np.ravel(sol['x']), sol['primal objective']

def prob6():
    """Solve the allocation model problem in 'ForestData.npy'.
    Note that the first three rows of the data correspond to the first
    analysis area, the second group of three rows correspond to the second
    analysis area, and so on.

    Returns (in order):
        The optimizer x (ndarray)
        The optimal value (sol['primal objective']*-1000)
    """
    data = np.load('ForestData.npy')

    c = matrix(data[:,3]*-1)

    A = la.block_diag(*[[1.,1.,1.] for _ in xrange(7)])
    b = data[::3,1].copy()

    G = np.vstack((-data[:,4], -data[:,5], -data[:,6], -np.eye(21))) # flip the inequality signs
    h = np.hstack(([-40000., -5., -70.*788.], np.zeros(21)))         # flip the inequality signs

    c = matrix(c)
    A = matrix(A)
    b = matrix(b)
    G = matrix(G)
    h = matrix(h)

    sol = solvers.lp(c,G,h,A,b)
    return np.ravel(sol['x']), sol['primal objective']*-1000.

    # Answers:
    # np.array([[ 1.41e-08],[ 6.76e-08],[ 7.50e+01],[ 9.00e+01],[ 1.28e-07],
    #           [ 2.52e-07],[ 1.40e+02],[ 4.18e-07],[ 5.52e-06],[ 1.04e-08],
    #           [ 8.94e-09],[ 6.00e+01],[ 1.23e-07],[ 1.54e+02],[ 5.80e+01],
    #           [ 3.16e-08],[ 3.58e-08],[ 9.80e+01],[ 1.63e-08],[ 9.12e-09],
    #           [ 1.13e+02]])
    # 322514998.983

''' # Generate the forest data.
forest=np.array([[1,75.,1,503.,310.,0.01,40],
[0,0,2,140,50,0.04,80],
[0,0,3,203,0,0,95],
[2,90.,1,675,198,0.03,55],
[0,0,2,100,46,0.06,60],
[0,0,3,45,0,0,65],
[3,140.,1,630,210,0.04,45],
[0,0,2,105,57,0.07,55],
[0,0,3,40,0,0,60],
[4,60.,1,330,112,0.01,30],
[0,0,2,40,30,0.02,35],
[0,0,3,295,0,0,90],
[5,212.,1,105,40,0.05,60],
[0,0,2,460,32,0.08,60],
[0,0,3,120,0,0,70],
[6,98.,1,490,105,0.02,35],
[0,0,2,55,25,0.03,50],
[0,0,3,180,0,0,75],
[7,113.,1,705,213,0.02,40],
[0,0,2,60,40,0.04,45],
[0,0,3,400,0,0,95]])
np.save('ForestData',forest)
'''

# END OF SOLUTIONS ============================================================

def test(student_module):
    """Test script. Import the student's solutions file as a module.

    10 points for problem 1
    10 points for problem 2
    10 points for problem 3
    10 points for problem 4
    10 points for problem 5
    20 points for problem 6

    Inputs:
        student_module: the imported module for the student's file.

    Returns:
        score (int): the student's score, out of 70.
        feedback (str): a printout of test results for the student.
    """
    tester = _testDriver()
    tester.test_all(student_module)
    return tester.score, tester.feedback

class _testDriver(object):
    """Class for testing a student's work. See test.__doc__ for more info."""

    # Constructor -------------------------------------------------------------
    def __init__(self):
        """Initialize the feedback attribute."""
        self.feedback = ""

    # Main routine -----------------------------------------------------------
    def test_all(self, student_module, total=70):
        """Grade the provided module on each problem and compile feedback."""
        # Reset feedback and score.
        self.feedback = ""
        self.score = 0

        def test_one(problem, number, value):
            """Test a single problem, checking for errors."""
            try:
                self.feedback += "\n\nProblem {} ({} points):".format(
                                                                number, value)
                points = problem(student_module)
                self.score += points
                self.feedback += "\nScore += {}".format(points)
            except BaseException as e:
                self.feedback += "\n{}: {}".format(self._errType(e), e)

        # Grade each problem.
        test_one(self.problem1, 1, 10)  # Problem 1: 10 points.
        test_one(self.problem2, 2, 10)  # Problem 2: 10 points.
        test_one(self.problem3, 3, 10)  # Problem 3: 10 points.
        test_one(self.problem3, 4, 10)  # Problem 4: 10 points.
        test_one(self.problem3, 5, 10)  # Problem 5: 10 points.
        test_one(self.problem4, 6, 20)  # Problem 6: 20 points.

        # Report final score.
        percentage = (100. * self.score) / total
        self.feedback += "\n\nTotal score: {}/{} = {}%".format(
                                    self.score, total, round(percentage, 2))
        if   percentage >=  98: self.feedback += "\n\nExcellent!"
        elif percentage >=  90: self.feedback += "\n\nGreat job!"

        # Add comments (optionally).
        print(self.feedback)
        comments = str(raw_input("Comments: "))
        if len(comments) > 0:
            self.feedback += '\n\n\nComments:\n\t{}'.format(comments)

    # Helper Functions --------------------------------------------------------
    @staticmethod
    def _errType(error):
        """Get just the name of the exception 'error' in string format."""
        if isinstance(error, BaseException):
            return str(type(error)).lstrip("<type 'exceptions.").rstrip("'>")
        else:
            return str(error)

    def _eqTest(self, correct, student, message):
        """Test to see if 'correct' and 'student' are equal.
        Report the given 'message' if they are not.
        """
        if student is None:
            self.feedback += "\nFailed to return a value."
            return 0
        if np.allclose(correct, student, atol=1e-1, rtol=1e-1):
            return 1
        else:
            self.feedback += "\n{}".format(message)
            self.feedback += "\n\tCorrect response:\n{}".format(correct)
            self.feedback += "\n\tStudent response:\n{}".format(student)
            return 0

    # Problems ----------------------------------------------------------------
    def _test_problem(self, s, ans_1, ans_2, stu, index):
        """Test a problem with solutions 'ans' and submission 'stu'."""

        # Attempt to unpack the student's solutions values.
        try:
            stu_1, stu_2 = stu
        except (TypeError, ValueError):
            self.feedback += "\nprob{}() must return 2 values.".format(index)
            raise

        # Test the student's values against the true solution.
        points = 0
        points += 5*self._eqTest(ans_1, stu_1, "Incorrect optimizer")
        points += 5*self._eqTest(ans_2, stu_2, "Incorrect optimial value")
        print("PROBLEM {}: {}".format(index, points))
        return points

    def problem1(self, s):
        """Test prob1(). 10 points."""
        return self._test_problem(s,
                        np.array([[1.54643957],[ 1.20887997],[ 1.89941363]]),
                        10, s.prob1(), 1)

    # np.array([ -1.14760916e-09,   1.50000000e+00,   6.95278285e-11])
    # 1.4999999996249482

    def problem3(self, s):
        """Test prob3(). 10 points."""
        return self._test_problem(s,
                        np.array([[ 5.00e+00],[ 2.00e+00],[ -7.03e-08],
                                  [ 2.00e+00],[ -5.44e-09],[ 4.00e+00]]),
                        86, s.prob3(), 3)

    def problem4(self, s):
        """Test prob4(). 10 points."""
        return self._test_problem(s, np.array([[-1.50],[ 1.00],[-.5]]),
                        -2.5, s.prob4(), 4)

    def problem6(self, s):
        """Test prob6(). 20 points."""
        return 2*self._test_problem(s,
                        np.array([[ 1.41e-08],[ 6.76e-08],[ 7.50e+01],
                                  [ 9.00e+01],[ 1.28e-07],[ 2.52e-07],
                                  [ 1.40e+02],[ 4.18e-07],[ 5.52e-06],
                                  [ 1.04e-08],[ 8.94e-09],[ 6.00e+01],
                                  [ 1.23e-07],[ 1.54e+02],[ 5.80e+01],
                                  [ 3.16e-08],[ 3.58e-08],[ 9.80e+01],
                                  [ 1.63e-08],[ 9.12e-09],[ 1.13e+02]]),
                        322514998.983, s.prob6(), 6)

# END OF FILE =================================================================


import numpy as np
import scipy.optimize as opt
import matplotlib.pyplot as plt
from scipy.io import loadmat

def sigmoide(X):
    return 1/(1+np.exp(-X))

def hipotesis(X, Theta):
    return sigmoide(np.dot(X, np.transpose(np.array([Theta]))))

def coste(Theta, X, Y, reg):
    m = np.shape(X)[0]
    H = hipotesis(X, Theta)
    aux = Y*np.log(H + 1e-6) + (1-Y)*np.log(1 - H + 1e-6)
    aux = -aux.sum()/m
    aux2 = np.sum((Theta ** 2))
    aux2 = (reg/(2*m))*aux2
    return aux + aux2

def coste2(Theta,X,Y):
    H = sigmoide(np.matmul(X, Theta))
    return (- 1 / (len(X))) * (np.dot(Y, np.log(H)) + np.dot((1 - Y), np.log(1 - H)))

def gradienteRecurs(Theta, X, Y, reg):
    m = np.shape(X)[0]
    grad = np.ravel((1/m)*np.dot(np.transpose(X), (hipotesis(X,Theta) - Y))) #+ (reg/m)*Theta
    grad[0] = (1/m)*np.sum((hipotesis(X,Theta) - Y) * X[:,0:1])
    return grad

def fun(thetas, X, etiq):
    return np.argmax(np.dot(thetas, X)) + 1 == etiq

def oneVsAll(Xp, Yp, num_etiquetas, reg):
    n = np.shape(Xp)[1]
    thetas = np.empty((0,n), float)
    ies = np.arange(1, num_etiquetas + 1)
    for i in ies:
        Y = np.copy(Yp)
        Theta = np.zeros(n)
        tr = np.where(Yp == i)
        fls = np.where(Yp != i)
        X = Xp
        Y[tr[0]] = 1
        Y[fls[0]] = 0
        print(Y)
        result = opt.fmin_tnc(func=coste, x0=Theta, fprime=gradienteRecurs, args=(X, Y, reg))
        thetas = np.vstack((thetas, result[0]))
    return thetas

data = loadmat("ex3data1.mat")
X = data['X']
Y = data['y']
Y = Y.astype(int)
m = np.shape(X)[0]

sample = np.random.choice(X.shape[0],10)
plt.imshow(X[sample,:].reshape(-1,20).T)
plt.axis('off')
plt.savefig('prueba.png')

X = np.hstack([np.ones([m,1]), X])

thetas = oneVsAll(X, Y, 10, 0.1)

aux = [fun(thetas, X[i], Y[i][0]) for i in range(m)]

print("Sol -->", np.sum(aux)/m)

#i = 756

#calculo = np.dot(thetas, X[i])

#print("Sol -->", np.argmax(calculo) + 1, "realmente es ", Y[i])

print("FIN")


import numpy as np

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

for e in L:
    print(e)

for e in A:
    print(e)

L.append(4)
L = L + [5]

L2 = []
for e in L:
    L2.append(e+e)

A+A
2*A

2*L

L3 = []
for e in L:
    L3.append(e*e)

A**2
np.sqrt(A)
np.log(A)
np.exp(A)

a = np.array([1,2])
b = np.array([2,1])

dot = 0
for e,f in zip(a,b):
    dot += e*f

# elementwise multiplication
a*b

# dot product (inner product)
np.sum(a*b)
(a*b).sum()

np.dot(a,b)
a.dot(b)
b.dot(a)
np.inner(a,b)

# magnitude of a vector
amag = np.sqrt((a*a).sum())
amag = np.linalg.norm(a)

# angle between two vectors
cosangle = a.dot(b) / (np.linalg.norm(a)*np.linalg.norm(b))
angle = np.arccos(cosangle)

# matrix
L = [[1,2], [3,4]]
M = np.array(L)

L[0]
L[0][0]

M[0][0]
M[0,0]

M2 = np.matrix(L) # recommended: use np.array instead!!!

A = np.array(M2)
A

# transpose
A.T

# generating arrays and matrices
Z = np.zeros(10)
ZM = np.zeros((10,10))

OM = np.ones((10,10))

RM = np.random.random((10,10)) # uniformly distributed numbers between 0..1
GM = np.random.randn(10,10) # Gaussian distribution (note: no tuple!)

GM.mean()
GM.var()

# elementwise matrix multiplication:
A = np.array([[1,2],[3,4]])
B = np.array([[5,6],[7,8]])
A*B

# matrix multiplication:
A = np.array([[1,2],[3,4]])
B = np.array([[5,6,7],[7,8,9]])
A.dot(B)

# inverse:
Ainv = np.linalg.inv(A)
Ainv.dot(A)
A.dot(Ainv)

# determinant:
np.linalg.det(A)

# diagonal elements of a matrix:
np.diag(A)

# constructing a diagonal matrix:
np.diag([1,2])

# outer product:
a = np.array([1,2])
b = np.array([3,4])
np.outer(a,b)

# trace (sum of the diagnoal elements):
np.diag(A).sum()
np.trace(A)

# covariance:
X = np.random.randn(100,3)
cov = np.cov(X)
cov.shape
covT = np.cov(X.T)
covT.shape

# eigenvalues, eigenvectors:
np.linalg.eigh(covT) # eigh is for symmetric (A=A^T) and Hermitian (A=A^H) matrices
np.linalg.eig(covT) # eig is for general matrices

# solving a linear system of equations:
A = np.array([[1,2], [3,4]])
b = np.array([1,2])
x = np.linalg.inv(A).dot(b)

np.linalg.solve(A, b)


#Mutlistep method: Adams Bashforth Method
import numpy as np

def dy(x,y):
    dyvalue = x - y**2
    return dyvalue

x0= 0 ##least x value known
xp1 = 0.1
xp2 = 0.2
xp3 = 0.3 #highest x value known

xfinal= 0.3
h = 0.1
n = int(((xfinal - x0)/h)) #if they give limits

y0= 0.854 ## highest i of y value known
ym1 =  0.9145
ym2 = 1
ym3 = 0 ## least y value known

order = 2
##########
x = np.zeros((n+order,1))
y = np.zeros((n+order,1))

i =0

if order == 3:
    p = order -1
    y[p]= y0
    y[p-1]= ym1
    y[p-2]= ym2

    x[p]= xp2
    x[p-1]= xp1
    x[p-2]= x0
    while i<=(n+order):
        print("order3\n")
        y[p+1] = y[p] +(h/12)*(23*dy(x[p],y[p])-16*dy(x[p-1],y[p-1])+5*dy(x[p-2],y[p-2]))
        x[p+1] = x[p] + h
        print("y:"+str(i)+str(y[p+1]))

        p+=1
        i+=1
elif order == 2:
    p = order -1
    y[1]= y0
    y[0]= ym1

    x[1]= xp1
    x[0]= x0
    while i<=(n+order):
        print("order2\n")
        y[p+1] = y[p] +(h/2)*(3*dy(x[p],y[p])-dy(x[p-1],y[p-1]))
        x[p+1] = x[p] + h
        print("y:"+str(i)+str(y[p+1]))

        p+=1
        i+=1
elif order == 4:
    p = order-1
    y[3]= y0
    y[2]= ym1
    y[1]= ym2
    y[0]= ym3

    x[3]= xp3
    x[2]= xp2
    x[1]= xp1
    x[0]= x0
    while i<=(n+order):
        print("order4\n")
        y[p+1] = y[p] +(h/24)*(55*dy(x[p],y[p])-59*dy(x[p-1],y[p-1])+37*dy(x[p-2],y[p-2])-9*dy(x[p-3],y[p-3]))
        x[p+1] = x[p] + h
        print("y:"+str(i)+str(y[p+1]))

        p+=1
        i+=1


import numpy as np

def mean_square_error(y_true: np.ndarray, y_pred: np.ndarray) -> float:
    """
    Calculate MSE loss

    Parameters
    ----------
    y_true: ndarray of shape (n_samples, )
        True response values
    y_pred: ndarray of shape (n_samples, )
        Predicted response values

    Returns
    -------
    MSE of given predictions
    """
    los_sum = 0
    for i in range(len(y_true)):
        los_sum += ((y_true[i] - y_pred[i]) ** 2)

    return los_sum / len(y_true)

def misclassification_error(y_true: np.ndarray, y_pred: np.ndarray,
                            normalize: bool = True) -> float:
    """
    Calculate misclassification loss

    Parameters
    ----------
    y_true: ndarray of shape (n_samples, )
        True response values
    y_pred: ndarray of shape (n_samples, )
        Predicted response values
    normalize: bool, default = True
        Normalize by number of samples or not

    Returns
    -------
    Misclassification of given predictions
    """
    size = y_pred.size
    error_sum = 0

    for i in range(size):
        if y_pred[i] != y_true[i]:
            error_sum += 1

    return error_sum / size if normalize else error_sum

def accuracy(y_true: np.ndarray, y_pred: np.ndarray) -> float:
    """
    Calculate accuracy of given predictions

    Parameters
    ----------
    y_true: ndarray of shape (n_samples, )
        True response values
    y_pred: ndarray of shape (n_samples, )
        Predicted response values

    Returns
    -------
    Accuracy of given predictions
    """
    accurate_sum = 0
    for i in range(y_true.size):
        if y_true[i] == y_pred[i]:
            accurate_sum += 1

    return accurate_sum / y_true.size

def cross_entropy(y_true: np.ndarray, y_pred: np.ndarray) -> float:
    """
    Calculate the cross entropy of given predictions

    Parameters
    ----------
    y_true: ndarray of shape (n_samples, )
        True response values
    y_pred: ndarray of shape (n_samples, )
        Predicted response values

    Returns
    -------
    Cross entropy of given predictions
    """
    raise NotImplementedError()

if __name__ == '__main__':
    y_true = np.array([279000, 432000, 326000, 333000, 437400, 555950])
    y_pred = np.array(
        [199000.37562541, 452589.25533196, 345267.48129011, 345856.57131275,
         563867.1347574, 395102.94362135])

    print(mean_square_error(y_true, y_pred))


"""
Demonstration of Python Scientific computing.

Uses Newton Raphson's method as a vehicle.
"""

import matplotlib.pyplot as plt
import scipy as sp

def myfunc(x):
    """
    Generic function.

    Parameters
    ----------
    x: float or array_like
        The value or array at which to calculate the function

    Examples
    --------

    Execution on a float

    >>> from newton_raphson import *
    >>> myfunc(3.0)
        11.0

    Execution on a SciPy array_like

    >>> a = np.linspace(0,10,10)
    >>> myfunc(a)
        array([  -4.        ,   -0.54320988,    5.38271605,   13.77777778,
             24.64197531,   37.97530864,   53.77777778,   72.04938272,
             92.79012346,  116.        ])
    """
    return x**2+2*x-4

def newton_raphson_plot(function, x0=0, dx=1e-10, eps=1e-10):
    """
    Solve for a root of a function using Newton Raphson's method.

    Also plots the process.

    Parameters
    ----------
    function : string
        String with name of function to be solved for *function(x) = 0*
    x0       : float
        Initial guess for *x* near *function(x) = 0*
    dx       : float
        Delta x used for finite difference calculation of slope
    eps      : float
        Absolute value of *function(x)* which is considered zero.

    Examples
    --------
    >>> from newton_raphson import *
    >>> def myfunc(x):
    ...     return x**2+2*x-4
    >>> function_name = 'myfunc'
    >>> newton_raphson_plot(function_name, x0=2)
        (1.23606797..., ...)
    """
    deltax = 2 * eps
    count = 0
    x = x0
    y = np.linspace(1, 6, 200)
    plt.plot(y, globals()[function](y))
    plt.ylabel('$f(x)$')
    plt.xlabel('$x$')
    plt.title('Newton Raphson search for solution to $f(x)=0$.')
    plt.grid(True)
    plt.plot(np.array([x0, x0]), np.array([globals()[function](x0), 0]), 'r')
    plt.plot(np.array([x0]), np.array([globals()[function](x0)]), 'r*')
    while abs(globals()[function](x)) > eps and count < 50:
        count += 1
        plt.plot(np.array([x, x]), np.array([globals()[function](x), 0]), 'r')
        plt.plot(np.array([x]), np.array([globals()[function](x)]), 'r*')
        f = globals()[function](x)
        f2 = globals()[function](x + dx)
        dfdx = (f2 - f) / dx
        deltax = -f / dfdx
        x = x + deltax
        xr = np.linspace(x, x - deltax, 200)
        y = xr * dfdx - x * dfdx
        plt.plot(xr, y, 'y')  # Current point
    return x, deltax

def newton_raphson(function, x0=0, dx=1e-10, eps=1e-10):
    """
    Solve for a root of a function using Newton Raphson's method.

    Parameters
    ----------
    function : string
        String with name of function to be solved for *function(x) = 0*
    x0       : float
        Initial guess for *x* near *function(x) = 0*
    dx       : float
        Delta x used for finite difference calculation of slope
    eps      : float
        Absolute value of *function(x)* which is considered zero.

    Examples
    --------
    >>> from newton_raphson import *
    >>> def myfunc(x):
    ...     return x**2+2*x-4
    >>> function_name = 'myfunc'
    >>> newton_raphson(function_name, x0=2)
        (1.2360679..., ...)
    """
    deltax = 2*eps
    count = 0
    x = x0
    # loop until it converges, but no more than 50 times
    while abs(deltax) > eps and count < 50:
        count += 1 # I can add 1 to the variable *count*. Neat Python shortcut.
        # This is a comment
        # The next line is "Matlab style" and *bad*
        #f = eval(function + '('+ str(x) + ')')
        f = globals()[function](x)  #We explain later.
        #f2 = eval(function + '('+ str(x+dx) + ')')
        f2 = globals()[function](x+dx)
        dfdx = (f2-f)/dx
        deltax = -f/dfdx
        x = x + deltax
    return x, deltax


"""
This is a simple python program to generate pseudo random numbers between 0 and 1 using Linear Congruent method
"""

import numpy as np
import matplotlib.pyplot as plt

def rand_no_gen(seed, a, c, m, no_of_randnos):
    """
    This function generates a numpy array of random numbers (positive integers) using Linear congruent method

    If r_i is initial random number, the next random number in the sequence is given by
    r_i+1 = (a*r_i + c) % m
    The random numbers generated will start repeating after m numbers
    Parameters
    ----------
    seed : int
        The starting 'seed' for the random no. generator
    a : int
        The multiplying parameter for the random no. generator
    c : int
        The adding parameter for the random no. generator
    m : int
        m determines how many number of random nos can be generated. Repetition starts after m entries
    no_of_randnos : int
        Total no of random numbers (including seed) required

    Returns
    -------
    randno_array : ndarray
        A numpy array of the random nos generated

    """
    randno_array = np.zeros(no_of_randnos)
    randno_array[0] = seed
    for i in range(no_of_randnos - 1):
        randno_array[i + 1] = (a * randno_array[i] + c) % m
    return randno_array

def random_generator_test(array_of_random_numbers):
    if len(array_of_random_numbers) % 2 != 0:
        mod_array = array_of_random_numbers[0:-1]
    else:
        mod_array = array_of_random_numbers
    x_values = []
    y_values = []

    for i in range(len(mod_array)):
        if i % 2 == 0:
            x_values.append(mod_array[i])
        else:
            y_values.append(mod_array[i])

    fig, ax = plt.subplots()
    ax.scatter(x_values, y_values)
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_title('Correlations of random nos')
    ax.grid(True)
    plt.show()

def main():
    print('## A simple python script to generate random numbers ##')
    no_of_randnos = int(input('Enter the number of random numbers that you want: '))
    seed = int(input('Enter seed value: '))
    a = int(input("Enter 'a' parameter for the generator: "))
    c = int(input("Enter 'c' parameter for the generator: "))
    m = int(input("Enter 'm' parameter for the generator: "))
    rand_array = rand_no_gen(seed, a, c, m, no_of_randnos)
    print(f'The generated random numbers are {rand_array}')
    random_generator_test(rand_array)

if __name__ == '__main__':
    main()


from scipy import stats
import matplotlib.pyplot as plt
import numpy as np
import jax.scipy.stats as jstats
from jax import grad

def main():

    # various beta distribution shapes
    x_vals = np.linspace(0.0, 1.0, 100)
    plt.plot(x_vals, stats.beta.pdf(x_vals, a=0.5, b=0.5), label=f'a={0.5}, b={0.5}')
    plt.plot(x_vals, stats.beta.pdf(x_vals, a=1.0, b=1.0), label=f'a={1.0}, b={1.0}')
    plt.plot(x_vals, stats.beta.pdf(x_vals, a=10.0, b=10.0), label=f'a={10.0}, b={10.0}')
    plt.plot(x_vals, stats.beta.pdf(x_vals, a=5.0, b=20.0), label=f'a={5.0}, b={20.0}')
    plt.plot(x_vals, stats.beta.pdf(x_vals, a=20.0, b=5.0), label=f'a={20.0}, b={5.0}')
    plt.ylabel('Beta PDF')
    plt.xlabel('Action')
    plt.legend()
    plt.show()

    # action distribution
    plt.plot(x_vals, stats.beta.pdf(x_vals, a=2.0, b=2.0), label=f'a={2.0}, b={2.0}')
    plt.axvline(0.1, color='red')
    plt.legend()
    plt.xlabel('Action')
    plt.ylabel('Beta PDF')
    plt.show()

    # beta distribution at x=0.1 for varying values of a
    a_vals = np.linspace(0.1, 2.0, 100)
    pdf_at_x = [stats.beta.pdf(x=0.1, a=a, b=2.0) for a in a_vals]
    plt.plot(a_vals, pdf_at_x)
    plt.xlabel('a')
    plt.ylabel('Beta PDF @ x=0.1, b=2.0')
    plt.xlim((0.1, 2.0))
    plt.show()

    # modified beta distribution to increase the density at x=0.1
    plt.plot(x_vals, stats.beta.pdf(x_vals, a=2.0, b=2.0), label=f'a={2.0}, b={2.0}')
    plt.plot(x_vals, stats.beta.pdf(x_vals, a=0.6, b=2.0), label=f'a={0.6}, b={2.0}')
    plt.axvline(0.1, color='red')
    plt.legend()
    plt.xlabel('Action')
    plt.ylabel('Beta PDF')
    plt.show()

    # 1. Define the function for which we want a gradient.
    def jax_beta_pdf(
            x: float,
            a: float,
            b: float
    ):
        return jstats.beta.pdf(x=x, a=a, b=b, loc=0.0, scale=1.0)

    # 2. Ask JAX for the gradient with respect to the second argument (shape parameter a).
    jax_beta_pdf_grad = grad(jax_beta_pdf, argnums=1)

    # 3. Calculate the gradient that we want.
    print(f'{jax_beta_pdf_grad(0.1, 2.0, b=2.0)}')

if __name__ == '__main__':
    main()

import numpy,from scipy,import jax,from jax


import numpy as np

def angular_distance(lam1, lam2, phi1, phi2):
    """
    Angular distance between points at lon-lat coordinates
    (lam1, phi1) and (lam2, phi2) in degrees.

    See https://en.wikipedia.org/wiki/Great-circle_distance
    """
    return (
        np.arccos(
            np.sin(phi1 * np.pi / 180) * np.sin(phi2 * np.pi / 180)
            + np.cos(phi1 * np.pi / 180)
            * np.cos(phi2 * np.pi / 180)
            * np.cos((lam2 - lam1) * np.pi / 180)
        )
        * 180
        / np.pi
    )

def fS_samp(r, N=100000):
    """
    Fractional spot coverage area computed by sampling a bunch
    of points on the sphere and computing the fraction that are
    inside a spot of radius `r`.

    See https://mathworld.wolfram.com/SpherePointPicking.html
    """
    u = np.random.random(N)
    v = np.random.random(N)
    lam = 360 * u - 180
    phi = np.arccos(2 * v - 1) * 180 / np.pi - 90
    delta_angle = angular_distance(0, lam, 0, phi)
    return np.array(
        [np.count_nonzero(delta_angle <= r_) / N for r_ in np.atleast_1d(r)]
    )

def fS_exact(r):
    """
    Analytical expression for the fractional spot coverage.

    """
    return 0.5 * (1 - np.cos(r * np.pi / 180))

def test_fS():
    r = np.linspace(0, 45, 100)
    assert np.allclose(fS_samp(r), fS_exact(r), atol=1e-2)


import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import os
import pyprobml_utils as pml

data_dir = "../data"
img = matplotlib.image.imread(os.path.join(data_dir, "clown.png"))

def rgb2gray(rgb):
    return np.dot(rgb[...,:3], [0.2989, 0.5870, 0.1140])

X = rgb2gray(img)

r = np.linalg.matrix_rank(X)
print(r)

U, sigma, V = np.linalg.svd(X, full_matrices=True)
ranks = [1, 2, 5, 10, 20, r]
R = len(ranks)

for i in range(R):
    k = ranks[i]
    #x_hat = np.matrix(U[:, :k]) * np.diag(sigma[:k]) * np.matrix(V[:k, :])
    x_hat = np.dot(np.dot(U[:, :k], np.diag(sigma[:k])), V[:k, :])
    plt.imshow(x_hat, cmap='gray')
    plt.title("rank {}".format(k))
    plt.axis("off")
    pml.savefig("svdImageDemoClown{}.pdf".format(k))
    plt.show()

k = 100
plt.plot(np.log(sigma[:k]), 'r-', linewidth=4, label="Original")
plt.ylabel(r"$log(\sigma_i)$")
plt.xlabel("i")

# Compare this to a random shuffled version of the image
x2 = np.random.permutation(X)
# so we convert to a 1d vector, permute, and convert back
x1d = X.ravel()
np.random.shuffle(x1d) # inplace
x2 = x1d.reshape(X.shape)
U, sigma2, V = np.linalg.svd(x2, full_matrices = False)
plt.plot(np.log(sigma2[:k]), 'b', linewidth=4, label="Randomized")
plt.legend()
pml.savefig("svdImageDemoClownSigmaScrambled.pdf")
plt.show()


import numpy as np
import matplotlib.pyplot as plt

"""
Generates a Geometric Brownian Motion timeseries.
Args:
    S0: the first term in the timeseries
    mu: the drift parameter of the GBM
    sigma: the volatility parameter of the GBM
    num_periods: the number of periods to model
    increments_per_period: the number of increments in the timeseries per period
"""
def gbm(S0, mu, sigma, num_periods, increments_per_period):
    T = num_periods
    dt = 1/increments_per_period
    N = num_periods*increments_per_period
    t = np.linspace(0, T, N)
    W = np.random.standard_normal(size = N)
    W = np.cumsum(W)*np.sqrt(dt) # standard brownian motion
    X = (mu-0.5*sigma**2)*t + sigma*W
    S = S0*np.exp(X) # geometric brownian motion
    return S

"""
Generates a cyclical timeseries, each cycle modeled independently by a Geometric Brownian Motion.
This is a useful function for simulating macroeconomic variables like GDP.
Args:
    S0: the first term in the timeseries
    mu_boom: the drift parameter of the GBM in a boom cycle (positive)
    mu_bust: the drift parameter of the GBM in a bust cycle (negative)
    sigma: the volatility parameter of the GBM
    cycle_lengths: an array of integers specifying the length of each cycle (number of periods)
    increments_per_period: the number of increments in the timeseries per period

cycle_lengths could be [2,4,3,4] for example, specifying four cycles with lenghts 2, 4, 3 and 4 periods respectively.
Cycles alternate between boom and bust. The first cycle is always a boom.
"""
def gbm_cyclical(S0, mu_boom, mu_bust, sigma, cycle_lengths, increments_per_period):
    cycle_lengths = np.array(cycle_lengths)
    cycle_lengths *= increments_per_period

    dt = 1
    mu_boom = mu_boom/increments_per_period
    mu_bust = mu_bust/increments_per_period
    sigma = np.sqrt(sigma**2/increments_per_period)

    cycles = np.array([[]])
    for i, cycle_length in enumerate(cycle_lengths):
        cycle_mu = mu_boom if i%2 == 0 else mu_bust
        cycle = np.exp((cycle_mu-sigma**2/2)*dt)*np.exp(sigma*np.random.normal(0, np.sqrt(dt), (1,cycle_length)))
        cycles = np.concatenate((cycles, cycle), axis=1)

    return S0*cycles.cumprod()

if __name__ == '__main__':
    # demonstrates the usage of gbm and gbm_cyclical
    np.random.seed(0)
    t = range(0, 520)
    S = gbm(1, 0.05, 0.5, 10, 52)
    S_cyclical = gbm_cyclical(1, 0.34, -0.2, 0.3, [2,3,5], 52)
    plt.plot(t, S, S_cyclical)
    plt.show()


"""
base.py
=======

Fundamental components for a simple python number theory library.

License Info
============

(c) David Lowry-Duda 2018 <davidlowryduda@davidlowryduda.com>

This is available under the MIT License. See
<https://opensource.org/licenses/MIT> for a copy of the license,
or see the home github repo
<https://github.com/davidlowryduda/pynt>.
"""
from typing import List, Tuple, Union
from itertools import product as cartesian_product
import numpy

def gcd(num1: int, num2: int) -> int:
    """
    Returns the greatest common divisor of `num1` and `num2`.

    Examples:
    >>> gcd(12, 30)
    6
    >>> gcd(0, 0)
    0
    >>> gcd(-1001, 26)
    13
    """
    if num1 == 0:
        return num2
    if num2 == 0:
        return num1
    if num1 < 0:
        num1 = -num1
    if num2 < 0:
        num2 = -num2
    # This is the Euclidean algorithm
    while num2 != 0:
        num1, num2 = num2, num1 % num2
    return num1

def smallest_prime_divisor(num: int, bound: Union[int, None] = None) -> int:
    """
    Returns the smallest prime divisor of the input `num` if that divisor is
    at most `bound`. If none are found, this returns `num`.

    Input:
        num: a positive integer
        bound: an optional bound on the size of the primes to check. If not
               given, then it defaults to `num`.

    Output:
        The smallest prime divisor of `num`, or `num` itself if that divisor is
        at least as large as `bound`.

    Raises:
        ValueError: if num < 1.

    Examples:
    >>> smallest_prime_divisor(15)
    3
    >>> smallest_prime_divisor(1001)
    7
    """
    if num < 1:
        raise ValueError("A positive integer is expected.")
    if num == 1:
        return num
    for prime in [2, 3, 5]:
        if num % prime == 0:
            return prime
    if bound is None:
        bound = num
    # Possible prime locations mod 2*3*5=30
    diffs = [6, 4, 2, 4, 2, 4, 6, 2]
    cand = 7
    i = 1
    while cand <= bound and cand*cand <= num:
        if num % cand == 0:
            return cand
        cand += diffs[i]
        i = (i + 1) % 8
    return num

# primesfrom2to(n) from
# https://stackoverflow.com/questions/2068372/fastest-way-to-list-all-primes-below-n
def primes(limit):
    """
    Returns the numpy array of primes up to (and not including) `limit`.

    Examples:
    >>> primes(10)
    array([2, 3, 5, 7])
    """
    sieve = numpy.ones(limit // 3 + (limit % 6 == 2), dtype=numpy.bool)
    for i in range(1, int(limit ** 0.5) // 3 + 1):
        if sieve[i]:
            k = (3 * i + 1) | 1
            sieve[k * k // 3::2 * k] = False
            sieve[k * (k - 2 * (i & 1) + 4) // 3:: 2 * k] = False
    return numpy.r_[2, 3, ((3 * numpy.nonzero(sieve)[0][1:] + 1) | 1)]

def factor(num: int) -> List[Tuple[int, int]]:
    """
    Returns the factorization of `num` as a list of tuples of the form (p, e)
    where `p` is a prime and `e` is the exponent of that prime in the
    factorization.

    Input:
        num: an integer to factor

    Output:
        a list of tuples (p, e), sorted by the size of p.

    Examples:
    >>> factor(100)
    [(2, 2), (5, 2)]
    >>> factor(-7007)
    [(7, 2), (11, 1), (13, 1)]
    >>> factor(1)
    []
    """
    if num in (-1, 0, 1):
        return []
    if num < 0:
        num = -num
    factors = []
    while num != 1:
        prime = smallest_prime_divisor(num)
        exp = 1
        num = num // prime
        while num % prime == 0:
            exp += 1
            num = num // prime
        factors.append((prime, exp))
    return factors

def factors(num: int) -> List[int]:
    """
    Returns the list of factors of an integer.

    Examples:
    >>> factors(6)
    [1, 2, 3, 6]
    >>> factors(30)
    [1, 2, 3, 5, 6, 10, 15, 30]
    """
    factorization = factor(num)
    primes_, exps = zip(*factorization)
    exp_choices = cartesian_product(*[range(exp+1) for exp in exps])
    ret = []
    for exp_choice in exp_choices:
        val = 1
        for prime, exp in zip(primes_, exp_choice):
            val *= (prime**exp)
        ret.append(val)
    return sorted(ret)


# log_posterior_probs function was implemented in October 2021 by
# Pascal Lieberherr, Zador Pataki, Timo Schönegg

import numpy
from scipy.stats import laplace, norm, t
import scipy
import math
import numpy as np
from scipy.special import logsumexp

VARIANCE = 2.0

normal_scale = math.sqrt(VARIANCE)
student_t_df = (2 * VARIANCE) / (VARIANCE - 1)
laplace_scale = VARIANCE / 2

HYPOTHESIS_SPACE = [norm(loc=0.0, scale=math.sqrt(VARIANCE)),
                    laplace(loc=0.0, scale=laplace_scale),
                    t(df=student_t_df)]

PRIOR_PROBS = np.array([0.35, 0.25, 0.4])

def generate_sample(n_samples, seed=None):
    """ data generating process of the Bayesian model """
    random_state = np.random.RandomState(seed)
    hypothesis_idx = np.random.choice(3, p=PRIOR_PROBS)
    dist = HYPOTHESIS_SPACE[hypothesis_idx]
    return dist.rvs(n_samples, random_state=random_state)

""" Solution """

from scipy.special import logsumexp

def log_posterior_probs(x):
    """
    Computes the log posterior probabilities for the three hypotheses, given the data x

    Args:
        x (np.ndarray): one-dimensional numpy array containing the training data
    Returns:
        log_posterior_probs (np.ndarray): a numpy array of size 3, containing the Bayesian log-posterior probabilities
                                          corresponding to the three hypotheses
    """
    assert x.ndim == 1

    # p1 = sum(log p(xi | H1)) + log(p(H1)) - log(prod(p(x1|H1))*p(H1)+prod(p(x2|H2))*p(H2)+prod(p(x3|H3))*p(H3))
    # p1 ~ sum(log p(xi | H1)) + log(p(H1)) - LSE(log(prod(p(x1|H1))*p(H1)), prod(p(x2|H2))*p(H2), prod(p(x3|H3))*p(H3))   # https://en.wikipedia.org/wiki/LogSumExp
    # p1 ~ sum(log p(xi | H1)) + log(p(H1)) - LSE(sum(log(p(x1|H1))) + log(p(H1)), ...)

    log_p_h1 = np.log(PRIOR_PROBS[0])
    log_p_h2 = np.log(PRIOR_PROBS[1])
    log_p_h3 = np.log(PRIOR_PROBS[2])

    sum_log_p_x_given_h1 = 0
    sum_log_p_x_given_h2 = 0
    sum_log_p_x_given_h3 = 0

    for xi in np.nditer(x):
        sum_log_p_x_given_h1 += np.log(norm.pdf(xi, loc=0.0, scale=math.sqrt(VARIANCE)))
        sum_log_p_x_given_h2 += np.log(laplace.pdf(xi, loc=0.0, scale=laplace_scale))
        sum_log_p_x_given_h3 += np.log(t.pdf(xi, df=student_t_df))

    lse_123 = logsumexp(np.array([sum_log_p_x_given_h1 + log_p_h1, sum_log_p_x_given_h2 + log_p_h2, sum_log_p_x_given_h3 + log_p_h3]))

    log_p_1 = sum_log_p_x_given_h1 + log_p_h1 - lse_123
    log_p_2 = sum_log_p_x_given_h2 + log_p_h2 - lse_123
    log_p_3 = sum_log_p_x_given_h3 + log_p_h3 - lse_123

    log_p = np.array([log_p_1, log_p_2, log_p_3])

    assert log_p.shape == (3,)
    return log_p

def posterior_probs(x):
    return np.exp(log_posterior_probs(x))

""" """

def main():
    """ sample from Laplace dist """
    dist = HYPOTHESIS_SPACE[1]
    x = dist.rvs(1000, random_state=28)

    print("Posterior probs for 1 sample from Laplacian")
    p = posterior_probs(x[:1])
    print("Normal: %.4f , Laplace: %.4f, Student-t: %.4f\n" % tuple(p))

    print("Posterior probs for 100 samples from Laplacian")
    p = posterior_probs(x[:50])
    print("Normal: %.4f , Laplace: %.4f, Student-t: %.4f\n" % tuple(p))

    print("Posterior probs for 1000 samples from Laplacian")
    p = posterior_probs(x[:1000])
    print("Normal: %.4f , Laplace: %.4f, Student-t: %.4f\n" % tuple(p))

    print("Posterior for 100 samples from the Bayesian data generating process")
    x = generate_sample(n_samples=100)
    p = posterior_probs(x)

    print("Normal: %.4f , Laplace: %.4f, Student-t: %.4f\n" % tuple(p))

if __name__ == "__main__":
    main()


# # Exponential functions and logarithms

import math
import numpy as np

# ## Exponential functions

# What is **e**? It is simply a number (known as Euler's number):

math.e

# **e** is a significant number, because it is the base rate of growth shared by all continually growing processes.
#
# For example, if I have **10 dollars**, and it grows 100% in 1 year (compounding continuously), I end up with **10\*e^1 dollars**:

# 100% growth for 1 year
10 * np.exp(1)

# 100% growth for 2 years
10 * np.exp(2)

# Side note: When e is raised to a power, it is known as **the exponential function**. Technically, any number can be the base, and it would still be known as **an exponential function** (such as 2^5). But in our context, the base of the exponential function is assumed to be e.
#
# Anyway, what if I only have 20% growth instead of 100% growth?

# 20% growth for 1 year
10 * np.exp(0.20)

# 20% growth for 2 years
10 * np.exp(0.20 * 2)

# ## Logarithms

# What is the **(natural) logarithm**? It gives you the time needed to reach a certain level of growth. For example, if I want growth by a factor of 2.718, it will take me 1 unit of time (assuming a 100% growth rate):

# time needed to grow 1 unit to 2.718 units
np.log(2.718)

# If I want growth by a factor of 7.389, it will take me 2 units of time:

# time needed to grow 1 unit to 7.389 units
np.log(7.389)

# If I want growth by a factor of 1, it will take me 0 units of time:

# time needed to grow 1 unit to 1 unit
np.log(1)

# If I want growth by a factor of 0.5, it will take me -0.693 units of time (which is like looking back in time):

# time needed to grow 1 unit to 0.5 units
np.log(0.5)

# ## Connecting the concepts

# As you can see, the exponential function and the natural logarithm are **inverses** of one another:

np.log(np.exp(5))

np.exp(np.log(5))


# @Time: 2022/4/13 21:18
# @Author: chang liu
# @Email: chang_liu_tamu@gmail.com
# @File:3.10.Performing_Matrix_and_Linear_algebra_calculations.py

import numpy as np
from numpy import linalg as nlg

m = np.arange(9).reshape((3, 3))
print(m)
print(m.T)
# print(nlg.inv(m))

v = np.arange(2, 5).reshape(3, 1)
print(v)

print(m * v)
print(m @ v)

print(nlg.det(m))
print(nlg.eigvals(m))

# nlg.solve(m, v) #solve linear systems


import numpy as np
from linear_programming.simplex_algorithm import SimplexAlgorithm

# object  min Z = -3 * x1 + x2 + x3
# s.t.
#       x1 - 2 * x2 +     x3 <= -11
#  -4 * x1 +     x2 + 2 * x3 >= 3
#   2 * x1 -              x3 = -1
#   x_i >= 0 (i = 1,2,3)

# A = np.array([[1, -2, 1], [-4, 1, 2], [2, 0, -1]])
# B = np.array([11, 3, -1])
# Z = np.array([-3, 1, 1])  # min
# restrict = ['<=', '>=', '=']
# mode = 'min'

# A = np.array([[-1, 2], [0, 1]])
# B = np.array([2, 3])
# Z = np.array([1, 2]) # max
# restrict = ['<=', '<=']
# mode = 'max'

# A = np.array([[1, 2, 2, 1, 0], [3, 4, 1, 0, 1]])
# B = np.array([8, 7])
# Z = np.array([5, 2, 3, -1, 1])
# restrict = ['=', '=']
# mode = 'max'

# A = np.array([[-2, 2, 1, 0], [3, 1, 0, 1]])
# B = np.array([4, 6])
# Z = np.array([3, 1, 1, 1])
# restrict = ['=', '=']
# mode = 'min'

# A = np.array([[4, 5], [5, 2]])
# B = np.array([100, 80])
# Z = np.array([10, 5])    # max
# restrict = ['<=', '<=']
# mode = 'max'

A = np.array([[1, 0], [1, 2], [0, 1]])
B = np.array([5, 10, 4])
Z = np.array([1, 3])    # max
restrict = ['<=', '<=', '<=']
mode = 'max'

if __name__ == '__main__':
    x, optimal = SimplexAlgorithm(A, B, Z, mode, restrict).run()
    print('最优解', x)
    print('目标函数最优值', optimal[0])