#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Interpolation module

We have analytic interpolation using SymPy and numerical interpolation
that returns an evaluable function.
"""
import sympy as sym

#%% Analytic interpolation
def basis_lagrange(x_data, var, i):
    """
    Compute the basis interpolant polynomial corresponding to the
    i-th  point given in x_data

    Parameters
    ----------
    x_data : list
        List with x coordinates for the interpolation.
    var : SymPy symbol
        Variable to be used for the interpolation
    i : int
        Point number for the interpolant.

    Returns
    -------
    poly : SymPy expression
        Interpolant polynomial in variable var.

    """
    prod = 1
    num = len(x_data)
    for j in range(num):
        if j == i:
            continue
        prod = prod * (var - x_data[j])/(x_data[i] - x_data[j])
    return sym.simplify(prod)

def lagrange_poly(x_data, y_data, var):
    """
    Compute the interpolant polynomial corresponding to the
    points given in (x_data, y_data)

    Parameters
    ----------
    x_data : list
        List with x coordinates for the interpolation.
    y_data : list
        List with y coordinates for the interpolation.
    var : SymPy symbol
        Variable to be used for the interpolation

    Returns
    -------
    poly : SymPy expression
        Interpolant polynomial in variable var.

    """
    poly = 0
    num = len(x_data)
    for i in range(num):
        poly = poly + y_data[i] * basis_lagrange(x_data, var, i)
    return poly

#%% Numerical interpolation
def lagrange(x_data, y_data):
    """
    Compute the interpolant polynomial corresponding to the
    points given in (x_data, y_data)

    Parameters
    ----------
    x_data : list
        List with x coordinates for the interpolation.
    y_data : list
        List with y coordinates for the interpolation.

    Returns
    -------
    fun_poly : Python function
        Interpolant polynomial as evaluable function.

    """
    def fun_poly(x):
        num = len(x_data)
        acu = 0
        for i in range(num):
            prod = 1
            for j in range(num):
                if i == j:
                    continue
                prod = prod * (x - x_data[j])/(x_data[i] - x_data[j])
            acu = acu + prod * y_data[i]
        return acu
    return fun_poly


import numpy as np

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

# matrix inverse, the inverse function is in the linalg module

a_inv = np.linalg.inv(A)
print(a_inv)

# verifying the Inverse by multiplying with A resulting in I

print(a_inv.dot(A))

# matrix determinant

det_a = np.linalg.det(A)
print(det_a)

# diagonal of a matrix

diag_a = np.diagonal(A) # gives diagonal elements in a vector
print(diag_a)

# to make a 2D array of diagonal

diag_2d = np.diag(diag_a)
print(diag_2d)

# remember that if you pass a 2D array to diag it will
# return diagonal elements in a 1D array

## if you pass a 1D array to diag, it will return a 2D array where the
## off diagonals are zero and the original array takes up the diagonal

#Outer Product

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

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

out_prod = np.outer(a,b)
print(out_prod)

# we can also do inner product like below, inner product is the same
# as outer product

inn_prod = np.inner(a,b)
print(inn_prod)

# Trace of a Matrix , is nothing but the sum of the elements of diagonal

#method1
print(A)
print(np.diag(A).sum())

#method2
print(np.trace(A))

x = np.random.randn(100,3) # by convention this is 100 observations
                           # 3 features
#to calculate covariance
cov = np.cov(x)
print(cov)

print(cov.shape) # this results in (100,100) which is wrong
                 # since we have only 3 features , it has to be (3,3)

print(np.cov(np.transpose(x)).shape) # this looks ugly

# there are two methods to calculate eigenvalues and eigenvectors

# np.eig(A) and np.eigh(A) ; eigh is used for symmetric matrices
# and hermitian matrices

# symmetric means a == transpose(a)

# hermitian means a == conjugate transpose ( a )

# since covariance is a symmetric matrix , we are using eigh

eig_cov = np.linalg.eigh(cov) # gives out a tuple containing
                              # in the first eigenvalues
                              # in the second corresponding eigenvectors
print(eig_cov)


from __future__ import print_function
from numpy import *
import numpy as np
from scipy.special import comb

# evaluates cubic bezier at t, return point
def q(ctrlPoly, t):
    return (1.0-t)**3 * ctrlPoly[0] + 3*(1.0-t)**2 * t * ctrlPoly[1] + 3*(1.0-t)* t**2 * ctrlPoly[2] + t**3 * ctrlPoly[3]

# evaluates cubic bezier first derivative at t, return point
def qprime(ctrlPoly, t):
    return 3*(1.0-t)**2 * (ctrlPoly[1]-ctrlPoly[0]) + 6*(1.0-t) * t * (ctrlPoly[2]-ctrlPoly[1]) + 3*t**2 * (ctrlPoly[3]-ctrlPoly[2])

# evaluates cubic bezier second derivative at t, return point
def qprimeprime(ctrlPoly, t):
    return 6*(1.0-t) * (ctrlPoly[2]-2*ctrlPoly[1]+ctrlPoly[0]) + 6*(t) * (ctrlPoly[3]-2*ctrlPoly[2]+ctrlPoly[1])

def bernstein_poly(i, n, t):
    """
     The Bernstein polynomial of n, i as a function of t
    """

    return comb(n, i) * ( t**(n-i) ) * (1 - t)**i

def bezier_curve(points, nTimes=1000):
    """
       Given a set of control points, return the
       bezier curve defined by the control points.

       points should be a list of lists, or list of tuples
       such as [ [1,1],
                 [2,3],
                 [4,5], ..[Xn, Yn] ]
        nTimes is the number of time steps, defaults to 1000

        See http://processingjs.nihongoresources.com/bezierinfo/
    """

    nPoints = len(points)
    xPoints = np.array([p[0] for p in points])
    yPoints = np.array([p[1] for p in points])

    t = np.linspace(0.0, 1.0, nTimes)

    polynomial_array = np.array([ bernstein_poly(i, nPoints-1, t) for i in range(0, nPoints)   ])

    xvals = np.dot(xPoints, polynomial_array)
    yvals = np.dot(yPoints, polynomial_array)

    return xvals, yvals

if __name__ == "__main__":
    from matplotlib import pyplot as plt

    nPoints = 4
    points = np.random.rand(nPoints,2)*200
    xpoints = [p[0] for p in points]
    ypoints = [p[1] for p in points]

    xvals, yvals = bezier_curve(points, nTimes=1000)
    plt.plot(xvals, yvals)
    plt.plot(xpoints, ypoints, "ro")
    for nr in range(len(points)):
        plt.text(points[nr][0], points[nr][1], nr)

    plt.show()


from __future__ import annotations  # See: https://stackoverflow.com/a/33533514

import numpy as np

class Vector(object):

    def __init__(self, coordinates: iter, tolerance: float = 1e-10) -> None:
        try:
            if not coordinates:
                raise ValueError

            self.coordinates = np.array(coordinates, dtype=np.float64)
            self.dimension = len(coordinates)
            self.tolerance = tolerance

        except ValueError:
            raise ValueError('The coordinates must be nonempty')

        except TypeError:
            raise TypeError('The coordinates must be an iterable')

    def __str__(self) -> str:
        return f'Vector: {self.coordinates}'

    def __eq__(self, other: Vector) -> bool:
        assert isinstance(other, Vector)

        return self.coordinates == other.coordinates

    def __add__(self, other: Vector) -> Vector:
        assert isinstance(other, Vector)
        assert self.dimension == other.dimension

        coord = [i + j for i, j in zip(self.coordinates, other.coordinates)]

        return Vector(coordinates=coord)

    def __sub__(self, other: Vector) -> Vector:
        assert isinstance(other, Vector)
        assert self.dimension == other.dimension

        coord = [i - j for i, j in zip(self.coordinates, other.coordinates)]

        return Vector(coordinates=coord)

    def scalar_mul(self, scalar: (int, float)) -> Vector:
        assert isinstance(scalar, (int, float))

        coordinates = [coord * scalar for coord in self.coordinates]

        return Vector(coordinates=coordinates)

    @property
    def magnitude(self) -> float:
        return np.sqrt(np.sum(np.power(self.coordinates, 2)))

    @property
    def direction(self) -> Vector:
        try:
            unit_coord = [coord / self.magnitude for coord in self.coordinates]
            return Vector(coordinates=unit_coord)

        except ZeroDivisionError:
            raise Exception('Cannot normalize the zero vector')

    def dot_product(self, other):
        assert isinstance(other, Vector)

        return sum(
            [i * j for i, j in zip(self.coordinates, other.coordinates)]
        )

    def get_angle_with(self, other: Vector, in_degrees: bool = False) -> float:
        assert isinstance(other, Vector)

        unit_dot_prod = self.direction.dot_product(other.direction)
        angle = np.arccos(unit_dot_prod)

        if in_degrees:
            angle *= 180/np.pi

        return angle

    @property
    def is_null_vector(self) -> bool:
        return self.magnitude < self.tolerance

    def is_parallel_to(self, other: Vector) -> bool:
        assert isinstance(other, Vector)

        return (
            self.is_null_vector or
            other.is_null_vector or
            self.get_angle_with(other=other) in (0, np.pi)
        )

    def is_orthogonal_to(self, other: Vector) -> bool:
        assert isinstance(other, Vector)

        return np.abs(self.dot_product(other=other)) < self.tolerance

    def component_parallel_to(self, basis):
        assert isinstance(basis, Vector)

        if self.is_null_vector or basis.is_null_vector:
            raise Exception('Cannot project from/to null vector')

        u = basis.direction
        weight = self.dot_product(other=u)

        return u.scalar_mul(scalar=weight)

    def component_orthogonal_to(self, basis: Vector) -> Vector:
        assert isinstance(basis, Vector)

        if self.is_null_vector or basis.is_null_vector:
            raise Exception('Cannot project from/to null vector')

        projection = self.component_parallel_to(basis=basis)
        rejection = self - projection

        return rejection

    @staticmethod
    def pad_third_dim(vector: Vector) -> Vector:
        if vector.dimension == 2:
            new_coordinates = vector.coordinates + (0, )
            vector = Vector(coordinates=new_coordinates)

        if vector.dimension != 3:
            raise Exception(f'Wrong number of dimensions for vector {vector}')

        return vector

    def cross_product(self, other: Vector) -> Vector:
        assert isinstance(other, Vector)

        x1, y1, z1 = self.pad_third_dim(vector=self).coordinates
        x2, y2, z2 = self.pad_third_dim(vector=other).coordinates

        return Vector([
            y1*z2 - y2*z1,
            -(x1*z2 - x2*z1),
            x1*y2 - x2*y1
        ])

    def get_area_parallelogram(self, other: Vector) -> float:
        assert isinstance(other, Vector)

        return self.cross_product(other=other).magnitude

    def get_area_triangle(self, other: Vector) -> float:
        assert isinstance(other, Vector)

        return self.get_area_parallelogram(other=other) / 2

if __name__ == '__main__':
    # Test case 1
    vector1 = Vector(coordinates=(8.218, -9.341))
    vector2 = Vector(coordinates=(-1.129, 2.111))
    print(vector1 + vector2)

    # Test case 2
    vector3 = Vector(coordinates=(7.119, 8.215))
    vector4 = Vector(coordinates=(-8.223, 0.878))
    print(vector3 - vector4)

    # Test case 3
    vector5 = Vector(coordinates=(1.671, -1.012, -0.318))
    print(vector5.scalar_mul(scalar=7.41))

    # Test case 4
    vector6 = Vector(coordinates=(-0.221, 7.437))
    print(vector6.magnitude)

    vector7 = Vector(coordinates=(8.813, -1.331, 6.247))
    print(vector7.magnitude)

    # Test case 5
    vector8 = Vector(coordinates=(5.581, -2.136))
    print(vector8.direction)

    vector9 = Vector(coordinates=(1.996, 3.108, -4.554))
    print(vector9.direction)

    # Test case 6
    vector10 = Vector(coordinates=(7.887, 4.138))
    vector11 = Vector(coordinates=(-8.802, 6.776))
    print(vector10.dot_product(other=vector11))

    vector12 = Vector(coordinates=(-5.955, -4.904, -1.874))
    vector13 = Vector(coordinates=(-4.496, -8.755, 7.103))
    print(vector12.dot_product(other=vector13))

    # Test case 7
    vector14 = Vector(coordinates=(3.183, -7.627))
    vector15 = Vector(coordinates=(-2.668, 5.319))
    print(vector14.get_angle_with(other=vector15))

    vector16 = Vector(coordinates=(7.35, 0.221, 5.188))
    vector17 = Vector(coordinates=(2.751, 8.259, 3.985))
    print(vector16.get_angle_with(other=vector17, in_degrees=True))

    # Test case 8
    vector18 = Vector(coordinates=(-7.579, -7.88))
    vector19 = Vector(coordinates=(22.737, 23.64))
    print('Are parallel?', vector18.is_parallel_to(other=vector19))
    print('Are orthogonal?', vector18.is_orthogonal_to(other=vector19))

    vector20 = Vector(coordinates=(-2.029, 9.97, 4.172))
    vector21 = Vector(coordinates=(-9.231, -6.639, -7.245))
    print('Are parallel?', vector20.is_parallel_to(other=vector21))
    print('Are orthogonal?', vector20.is_orthogonal_to(other=vector21))

    vector22 = Vector(coordinates=(-2.328, -7.284, -1.214))
    vector23 = Vector(coordinates=(-1.821, 1.072, -2.94))
    print('Are parallel?', vector22.is_parallel_to(other=vector23))
    print('Are orthogonal?', vector22.is_orthogonal_to(other=vector23))

    vector24 = Vector(coordinates=(2.118, 4.827))
    vector25 = Vector(coordinates=(0, 0))
    print('Are parallel?', vector24.is_parallel_to(other=vector25))
    print('Are orthogonal?', vector24.is_orthogonal_to(other=vector25))

    # Test case 9
    vector26 = Vector(coordinates=(3.039, 1.879))
    vector27 = Vector(coordinates=(0.825, 2.036))
    print(vector26.component_parallel_to(basis=vector27))

    # Test case 10
    vector28 = Vector(coordinates=(-9.88, -3.264, -8.159))
    vector29 = Vector(coordinates=(-2.155, -9.353, -9.473))
    print(vector28.component_orthogonal_to(basis=vector29))

    # Test case 11
    vector30 = Vector(coordinates=(3.009, -6.172, 3.692, -2.51))
    vector31 = Vector(coordinates=(6.404, -9.144, 2.759, 8.718))
    v_parallel = vector30.component_parallel_to(basis=vector31)
    v_orthogonal = vector30.component_orthogonal_to(basis=vector31)
    v = v_parallel + v_orthogonal
    print(v_parallel, v_orthogonal)

    # Test case 12
    vector32 = Vector(coordinates=(8.462, 7.893, -8.187))
    vector33 = Vector(coordinates=(6.984, -5.975, 4.778))
    print(vector32.cross_product(other=vector33))

    # Test case 13
    vector34 = Vector(coordinates=(-8.987, -9.838, 5.031))
    vector35 = Vector(coordinates=(-4.268, -1.861, -8.866))
    print(vector34.get_area_parallelogram(other=vector35))

    # Test case 14
    vector36 = Vector(coordinates=(1.5, 9.547, 3.691))
    vector37 = Vector(coordinates=(-6.007, 0.124, 5.772))
    print(vector36.get_area_triangle(other=vector37))


import numpy as np
'''
# min-max
a = np.array([[3,7,5],[8,4,3],[2,4,9]])
print("input array: \n", a)
print("min: ", np.amin(a))
print("min axis =0 ", np.amin(a, axis=0))
print("min axis =1 ", np.amin(a, axis=1))
print("max: ", np.amax(a))
print("max axis =0 ", np.amax(a, axis=0))
print("max axis =1 ", np.amax(a, axis=1))
'''
# TODO: tpt and percentile methods
'''
# median & mean
a = np.array([[30,65,70],[80,95,10],[50,90,60]])
print("input array: \n", a)
print("median: ", np.median(a))
print("median axis =0 ", np.median(a, axis=0))
print("median axis =1 ", np.median(a, axis=1))
print("mean: ", np.mean(a))
print("mean axis =0 ", np.mean(a, axis=0))
print("mean axis =1 ", np.mean(a, axis=1))
'''
'''
# weighted average

a = np.array([1,2,3,4])
wts = np.array([4,3,2,1])
print ("weighted avg: ", np.average(a,weights = wts))
'''

# std deviation
# Formula: std = sqrt(mean(abs(x - x.mean())**2))
print ("standard deviation: ", np.std([1,2,3,4]))

# variance
# Formula mean(abs(x - x.mean())**2)
print("variance: ", np.var([1,2,3,4]))


import pandas as pd
import numpy as np

def params_mle(data):
    '''
    Fit data to a Guassian distribution with Maximum Likelihood Estimation (MLE)

    Parameters
    ----------
    data : ndarray, list, dict, or DataFrame
        Data to be tested for normality

    Returns
    -------
    mle_params : Dataframe
        Dataframe with the first row containing the estimated means and
        the second row containing the estimated variance. The columns
        present the original variables in the data

    Examples
    --------
    iris_data = pd.DataFrame({"length": [1,2,3,4], "width": [5,6,7,8])
    make_qqplot(iris_data)
    '''

    ## PREPROCESSING
    ## =============

    # Set default column names as indexes
    try: # 2D data
        n_var = data.shape[1]
        var_names = range(n_var)
    except: # 1D data
        var_names = [0]

    # Address different input types
    if isinstance(data, pd.DataFrame):
        var_names = list(data)
        data = np.array(data)

    elif isinstance(data, pd.Series):
        if data.name != None:
            var_names = [data.name]
        data = np.array(data)

    elif isinstance(data, list):
        var_names = range(len(data))
        data = np.transpose(np.array(data))

    n_obs = data.shape[0]

    ## Calculations
    ## =============

    # Calculate mu estimates
    mu = np.sum(np.array(data), axis = 0)/n_obs

    # Calculate sigma estimates
    variance = np.sum((data - mu)**2, axis = 0)/n_obs
    sigma = variance**(1/2)

    ## Return results
    ## ==============
    mle_params = pd.DataFrame(np.vstack((mu, variance)), index = ["Mean", "Variance"], columns = var_names)
    return(mle_params)


from __future__ import division

import math
import numpy as np
from typing import Optional

def build_sinusoidal_positional_embedding(
    num_embeddings: int,
    embedding_dim: int,
    padding_idx: Optional[int] = None,
    dtype=np.float32
):
    """
    Build sinusoidal embeddings
    """
    half_dim = embedding_dim // 2
    emb = math.log(10000) / (half_dim - 1)
    emb = np.exp(-emb * np.arange(half_dim, dtype=dtype))
    emb = np.arange(num_embeddings, dtype=dtype)[:, None] * emb[None, :]
    emb = np.concatenate([np.sin(emb), np.cos(emb)], axis=1)
    emb = np.reshape(emb, [num_embeddings, -1])
    if embedding_dim % 2 == 1:
        # zero pad
        emb = np.concatenate(
            [emb, np.zeros(shape=[num_embeddings, 1], dtype=dtype)],
            axis=1
        )
    if padding_idx is not None:
        emb[padding_idx, :] = 0
    return emb


'''
How many defaults might we expect?
100xp

Let's say a bank made 100 mortgage loans. It is possible that anywhere between
0 and 100 of the loans will be defaulted upon. You would like to know the probability
of getting a given number of defaults, given that the probability of a default is
p = 0.05. To investigate this, you will do a simulation. You will perform 100 Bernoulli
trials using the perform_bernoulli_trials() function you wrote in the previous exercise
and record how many defaults we get. Here, a success is a default. (Remember that the
word "success" just means that the Bernoulli trial evaluates to True, i.e., did the loan
recipient default?) You will do this for another 100 Bernoulli trials. And again and again
until we have tried it 1000 times. Then, you will plot a histogram describing the probability
of the number of defaults.

Instructions
-Seed the random number generator to 42.
-Initialize n_defaults, an empty array, using np.empty(). It should contain 1000 entries,
since we are doing 1000 simulations.
-Write a for loop with 1000 iterations to compute the number of defaults per 100 loans using
the perform_bernoulli_trials() function. It accepts two arguments: the number of trials n -
in this case 100 - and the probability of success p - in this case the probability of a default,
which is 0.05. On each iteration of the loop store the result in an entry of n_defaults.
-Plot a histogram of n_defaults. Include the normed=True keyword argument so that the height of
the bars of the histogram indicate the probability.
-Show your plot.
'''
import numpy as np
import matplotlib.pyplot as plt

def perform_bernoulli_trials(n, p):
    """Perform n Bernoulli trials with success probability p
    and return number of successes."""
    # Initialize number of successes: n_success
    n_success = 0

    # Perform trials
    for i in range(n):
        # Choose random number between zero and one: random_number
        random_number = np.random.random()

        # If less than p, it's a success so add one to n_success
        if random_number < p:
            n_success += 1

    return n_success

# Seed random number generator
np.random.seed(42)

# Initialize the number of defaults: n_defaults
n_defaults = np.empty(1000)

# Compute the number of defaults
for i in range(1000):
    n_defaults[i] = perform_bernoulli_trials(100, 0.05)

# Plot the histogram with default number of bins; label your axes
_ = plt.hist(n_defaults, normed=True)
_ = plt.xlabel('number of defaults out of 100 loans')
_ = plt.ylabel('probability')

# Show the plot
plt.show()


import numpy as np

def EulerMethod(f, xi, yi, xmax, h):
    result = {}

    n = int((xmax - xi)/h) + 1

    x = np.linspace(xi, xmax, n)
    y = yi

    result[0] = {'x': x[0], 'y': y}

    for i in range(1, n):
        y = y + f(x[i-1], y) * h
        result[i] = {'x': x[i],'y': y}
    return result

def HeunsMethod(f_xy, xi, yi, h, xmax, its):
    n = int((xmax - xi)/h) + 1

    x = np.linspace(xi, xmax, n)
    y = yi

    result = {}
    result[0] = {'x': x[0], 'y': y}

    for i in range(1, n):
        y_p = y + f_xy(x[i-1], y) * h
        for _ in range(its):
            y_p = y + h * (f_xy(x[i-1], y) + f_xy(x[i], y_p))/2
        y = y_p
        result[i] = {'x': x[i],'y': y}
    return result

def MidPointMethod(f_xy, xi, yi, h, xmax):

    n = int((xmax - xi)/h) + 1

    x = np.linspace(xi, xmax, n)
    y = yi

    result = {}
    result[0] = {'x': x[0], 'y': y}

    for i in range(1, n):
        y_1b2 = y + f_xy(x[i-1], y) * h/2
        y = y + f_xy((x[i] + x[i-1])/2, y_1b2) * h
        result[i] = {'x': x[i],'y': y}
    return result

def Butcher_Tableau(method=None):
    if method == None:
        Meth_list = [   'Forward Euler',
                        'Explicit Midpoint',
                        'Ralston',
                        'Kutta-3rd',
                        'Classic-4th'   ]
        return Meth_list
    elif method == 'Forward Euler':
        C =  [  0   ]
        A = [[  0   ]]
        B =  [  1   ]
    elif method == 'Explicit Midpoint':
        C =  [  0,      0.5 ]
        A = [[  0,      0   ],
             [  0.5,    0   ]]
        B =  [  0 ,     1   ]
    elif method == 'Ralston':
        C =  [  0,      2/3 ]
        A = [[  0,      0   ],
             [  2/3,    0   ]]
        B =  [  1/4,    3/4 ]
    elif method == 'Kutta-3rd':
        C =  [  0,      1/2,    1   ]
        A = [[  0,      0,      0   ],
             [  1/2,    0,      0   ],
             [  -1,     2,      0   ]]
        B =  [  1/6,    2/3,    1/6 ]
    elif method == 'Classic-4th':
        C =  [  0,      1/2,    1/2,    1   ]
        A = [[  0,      0,      0,      0   ],
             [  1/2,    0,      0,      0   ],
             [  0,      1/2,    0,      0   ],
             [  0,      0,      1,      0   ]]
        B =  [  1/6,    1/3,    1/3,    1/6 ]

    return {'s':    len(C),
            'C':    np.array(C),
            'A':    np.array(A),
            'B':    np.array(B)}

def RungeKutta_General(F :list, xi :float, yi :list, h :float, xmax :float, Bt ):
    itr = int((xmax - xi)/h) + 1
    x = np.linspace(xi, xmax, itr)

    result = {}
    yn = yi
    var = len(yn)
    result[xi] = yn.copy()

    hk = np.zeros((var,Bt['s']))

    for n in range(itr - 1):
        xn = x[n]

        hk.fill(0)

        # k_i
        for i in range(Bt['s']):
            xt = xn + Bt['C'][i] * h
            yt = yn.copy()

            yt += hk.dot(Bt['A'][i])
            for m in range(var):
                hk[m, i] = h * F[m](xt, *yt)

        # y_{n+1}
        for i in range(var):
            yn[i] += np.array(Bt['B']).dot(hk[i])
        result[x[n+1]] = yn.copy()
    return result


import numpy as np

def hypergeometric(N, K):
    """
    This function compute the pmf of the Hypergeometric(N, K, n)
    for each possible value of n (i.e. n in {0,1,2,...,N})

    In the context where an urn with N marbles contains K white marbles,
    this function outputs an array P of shape (N+1,K+1) where
    P[i, j] correspond to the probabillity that with i samples without
    replacement we select j white marbles.

    Parameters
    ----------
    N : int
        The number of marbles in the urn
    K : int
        The number of white marbles in the urn

    Returns
    -------
    P : numpy.ndarray
        P[i, j] is the probability that with i samples without replacement
        we select j white marbles. P.shape is (N+1, K+1)

    Notes
    -----
    This is equivalent to
    np.array([scipy.stats.hypergeom(N, K, n).pmf(range(0,K+1)) for n in range(N+1)])
    but faster, if only a row is needed (e.g. P[i]) than using scipy is faster.

    This algorithm uses the following recursive formula
    P[i, j] = P[i-1, j]*(1-Q[i-1, j]) + P[i-1, j-1]*Q[i-1, j-1]
    with Q[i, j] = (K-j)/(N-i) the probability of sampling a white marble
    given that i marbles where sampled from which j were white
    """
    P = np.zeros((N+1, K+1))
    P[0,0] = 1 #P[0, j] = 1 if j==0 else 0
    P[N,K] = 1 #P[N, j] = 1 if j==K else 0

    #To compute Q We don't need to bother with
    #value > 1 since they will be multiplied by 0
    Q = (K-np.arange(K+1))[None]/(N-np.arange(N))[:,None]

    for n in range(1,N):
        P[n] = P[n-1]*(1-Q[n-1])
        P[n,1:] += P[n-1,:-1]*Q[n-1,:-1]

    return P

def superdupergeometric(N, K):
    """
    This is the scenario where we have an urn with N marbles
    and K of them are white. We sample from the urn without replacement
    until we obtain k white marbles. The function gives the probability
    that we need n samples to obtain k white marbles.

    Parameters
    ----------
    N : int
        The number of marbles in the urn
    K : int
        The number of white marbles in the urn

    Returns
    -------
    SP : numpy.ndarray
        SP[i, j] is the probability that it requires i samples without replacement
        to select j white marbles. SP.shape is (N+1, K+1)

    Notes
    -----
    Probabibly related to the negative hypergeometric distribution

    This uses the hypergeometric (hence the name)
    SP[i, j] = P[i-1, j-1]*Q[i-1, j-1]
    where P = hypergeometric(N, K) and with
    Q[i, j] = (K-j)/(N-i) the probability of sampling a white marble
    given that i marbles where sampled from which j were white
    """

    P = hypergeometric(N, K)
    SP = np.zeros((N+1, K+1))
    SP[0, 0] = 1 #we only need 0 sample to get 0 white marbles

    #To compute Q We don't need to bother with
    #value > 1 since they will be multiplied by 0
    Q = (K-np.arange(K+1))[None]/(N-np.arange(N))[:,None]

    for n in range(1,N+1):
        SP[n,1:] += P[n-1,:-1]*Q[n-1,:-1]

    return SP

def superdupergeometric_expectations(N, K):
    """
    This is the scenario where we have an urn with N marbles
    and K of them are white. We sample from the urn without replacement
    until we obtain k white marbles. The function gives the  expected
    number of samples n requires to get k white marbles (for each k).

    Parameters
    ----------
    N : int
        The number of marbles in the urn
    K : int
        The number of white marbles in the urn

    Returns
    -------
    ESP : numpy.ndarray
        ESP[k] is the expected number of samples without replacement
        requires to get k white marbles. ESP.shape is (K+1,)

    Notes
    -----
    This is equivalent (but way faster) to (SP*np.arange(N+1)[:,None]).sum(axis=0)
    where SP = superdupergeometric(N, K)
    """
    ESP = (N+1)/(K+1)*np.arange(K+1)
    return ESP


import numpy as np
import scipy.linalg as la
import time

def read_matrix_dimension():
    print("Enter dimension of matrix A(m >= n):", end=' ')
    try:
        m, n = (int(i) for i in input().strip().split())
        if m < n:
            print("Number of rows cannot be less than number of columns in LSP problem!")
            raise Exception()
    except Exception:
        print("Invalid input for matrix dimension. Try again by entering two space-separated integers:")
        return read_matrix_dimension()
    return m, n

def read_matrix(m, n):
    A = np.zeros((m, n), dtype=np.complex)
    print("Enter rows of the matrix A(separate elements of each row with space):")
    try:
        for i in range(m):
            A[i, :] = np.array([j for j in input().strip().split()]).astype(np.complex)
    except Exception as e:
        raise e
        print(e)
        print("Invalid input for matrix A. Please try again.")
        return read_matrix(m, n)
    return A

def read_vector(m):
    b = np.zeros(m, dtype=np.complex)
    print("Enter elements of the vector b of size {}(separate them with space):".format(m))
    try:
        b[:] = np.array([j for j in input().strip().split()]).astype(np.complex)
    except Exception:
        print("Invalid input for vector b. Please try again!")
        return read_vector(m)
    return b

def solve_lsp_using_svd(A, b, n, r):
    u, sigma, vt = la.svd(A)
    bbar = np.transpose(np.conj(u)).dot(b)
    y = np.zeros(n, dtype=np.complex)
    y[:r] = bbar[:r] / sigma[:r]
    if len(y) == 1:
        y = float(y)
    v = np.transpose(np.conj(vt))
    x = v.dot(y)
    remainder = la.norm(bbar[r:]) if r < n else 0
    return x.reshape((n,)), remainder

def solve_lsp_using_qr(A, b, n, r):
    q, R, p = la.qr(A, pivoting=True)
    bbar = np.transpose(np.conj(q)).dot(b)
    y = np.zeros(n, dtype=np.complex)
    y[:r] = la.solve(R[:r, :r], bbar[:r])
    x = y[p]
    remainder = la.norm(bbar[r:]) if r < n else 0
    return x.reshape((n,)), remainder

def lsp_solver(func, A, b, n, r, method):
    start = time.time()
    x, remainder = func(A, b, n, r)
    svd_time = time.time() - start
    print("Solved LSP using {} in {} ms. Here's the results:".format(method, np.round(svd_time * 1000, decimals=2)))
    print("x:", x)
    print("remainder:", remainder)

def __main__():
    m, n = read_matrix_dimension()
    A = read_matrix(m, n)
    b = read_vector(m)
    rank_A = np.linalg.matrix_rank(A)
    lsp_solver(solve_lsp_using_svd, A, b, n, rank_A, 'SVD')
    lsp_solver(solve_lsp_using_qr, A, b, n, rank_A, 'QR')

__main__()


from functools import partial
# import numpy as np

# solve equation with newton method
def newton(func, derFunc, initialValue, delta, epsilon, iterN):
    v = func(*[initialValue])
    print("k ", " x ", " f(x)")
    print(0, initialValue, v)
    if abs(v) < epsilon:
        return initialValue, v
    x0 = initialValue
    for i in range(0, iterN):
        x1 = x0 - v / derFunc(*[x0])
        v = func(*[x1])
        print(i + 1, x1, v)
        if abs(x1 - x0) < delta or abs(v) < epsilon:
            return x1, v
        else:
            x0 = x1
    return x1, v

# solve equation with bisection method
def bisection(func, a, b, delta, epsilon, iterN):
    mid = (a + b) * 0.5
    v = func(*[mid])
    print("k ", " x ", " f(x)")
    print(0, mid, v)
    if abs(v) < epsilon:
        return mid, v
    x0 = a
    x1 = b
    for i in range(0, iterN):
        f0 = func(*[x0])
        f1 = func(*[x1])
        if sign(f0) * sign(v) < 0:
            x1 = mid
        if sign(v) * sign(f1) < 0:
            x0 = mid
        mid = 0.5 * (x0 + x1)
        v = func(*[mid])
        print(i + 1, mid, v)
        if abs(x1 - x0) < delta or abs(v) < epsilon:
            return mid, v
    return mid, v

def sign(x):
    return -1 if x < 0 else 1 if x > 0 else 0

# a sum of a geometric progression with a1 = q = x
def sumFunc(sum, n, x):
    if x == 0 or x == 1:
        return n * x - sum
    return x * (1 - pow(x, n)) / (1 - x) - sum

# the derivative function of the sum function
def derFunc(n, x):
    if x == 0:
        return 1
    if x == 1:
        return (1 + n) * n / 2
    return (1 - (n + 1) * pow(x, n) + n * pow(x, n + 1)) / pow((1 - x), 2)

def main():
    n = 360
    sum = 75000 / 425.84
    iterN = 50
    initialValue = 0.5
    delta = 1e-6
    epsilon = 1e-12
    f = partial(sumFunc, sum, n)
    df = partial(derFunc, n)
    # newton(f, df, initialValue, delta, epsilon, iterN)
    bisection(f, 0, 1, epsilon, epsilon, 2 * iterN)

main()


import numpy as np
import pandas as pd
import scipy.optimize as op
import matplotlib.pyplot as plt

# read data
data = pd.read_csv('ex2data1.txt', header=None)

# show imported data details
# print('data = \n', data.head(10))
# print('**************************************')
# print('data.describe = \n', data.describe())
# print('**************************************')

# draw data
y = data.iloc[:, -1]

fig, ax = plt.subplots(figsize= (8,5))
plt.scatter(data.loc[y == 1, 0], data.loc[y == 1, 1], marker= '+', c= 'black',
            linewidths= 3, s= 100, label= 'Admitted')

plt.scatter(data.loc[y == 0, 0], data.loc[y == 0,1], marker= 'o', c= 'yellow',
            linewidths= 1, edgecolors= 'k', s= 80, label= 'Not admitted')

ax.set(title= 'Training Dataset', xlabel= 'Exam 1 score', ylabel= 'Exam 2 score')
ax.legend(loc= 1, shadow= True, borderpad= 1)
plt.show()

# Setup the data matrix appropriately, and add ones for the intercept term
data.insert(0, -1 , 1)
# print('\nnew data = \n', data.head(10))
# print('\n**************************************')

# separate X (training data) from y (target variable)
cols = data.shape[1]
X = data.iloc[:, 0 : cols-1]
y = data.iloc[:, cols-1 : cols]

# print('\nX data = \n', X.head(10))
# print('\n**************************************')
# print('\ny data = \n', y.head(10))
# print('\n**************************************')

# Convert data from data frames to numpy matrices
X = np.matrix(X.values)
y = np.matrix(y.values)

# Initialize fitting parameters
m, n = X.shape
initial_theta = np.zeros((n, 1))

# print('X \n', X)
# print('\nX.shape = ', X.shape)
# print('\n**************************************')
# print('y \n', y)
# print('\ny.shape = ', y.shape)
# print('\n**************************************')
# print('initial_theta \n', initial_theta)
# print('\ninitial_theta.shape = ', initial_theta.shape)
# print('\n**************************************')

# Compute and display initial cost and gradient
def sigmoid(z):
    return 1 / (1 + np.exp(-z))

nums = np.arange(-10, 10, 0.5)

fig, ax = plt.subplots(figsize= (8,5))
ax.plot(nums, sigmoid(nums), 'r')
ax.grid()
plt.show()

def costFunction(theta, X, y):
    m = len(y)
    theta = theta.reshape((X.shape[1],1))

    h = sigmoid(X * theta)
    J = ((y.T * np.log(h)) + ((1 - y).T * np.log(1 - h))) / (-m)
    return J

def Gradient(theta, X, y):
    theta = theta.reshape((X.shape[1],1))
    return ((X.T * (sigmoid(X * theta) - y)) / len(y))

cost = costFunction(initial_theta, X, y)
grad = Gradient(initial_theta, X, y)

print('\nCost at initial theta (zeros): %.3f' %cost)
print('Expected cost (approx): 0.693')
print('\n**************************************')

print('\nGradient at initial theta (zeros): ')
for i in grad.tolist(): print(' %.4f' %i[0])
print('Expected gradients (approx):\n -0.1000\n -12.0092\n -11.2628')
print('\n**************************************')

# Compute and display cost and gradient with non-zero theta
test_theta = np.array([[-24], [0.2], [0.2]])
cost = costFunction(test_theta, X, y)
grad = Gradient(test_theta, X, y)

print('\nCost at test theta: %.3f' %cost)
print('Expected cost (approx): 0.218')
print('\n**************************************')

print('\nGradient at test theta: ')
for i in grad.tolist(): print(' %.3f' %i[0])
print('Expected gradients (approx):\n 0.043\n 2.566\n 2.647')
print('\n**************************************')

# Run fminunc to obtain the optimal theta
# This function will return theta and the cost
Result = op.fmin_tnc(func= costFunction,
                     x0= initial_theta,
                     args= (X, y),
                     fprime= Gradient,
                     disp= False)

theta = Result[0].reshape((X.shape[1], 1))

print('\nNew Theta After Optimization = ')
for i in range(theta.shape[0]): print(' %.3f' %theta[i, 0])
print('Expected theta (approx):')
print(' -25.161\n 0.206\n 0.201')
print('\n**************************************')

newCost = costFunction(Result[0], X, y)
print('\nNew Cost After Optimization = %.3f' %newCost)
print('Expected cost (approx): 0.203')
print('\n**************************************')

# Plot Boundary
# Plot Data
fig, ax = plt.subplots(figsize= (8,5))
plt.scatter(data.loc[y == 1, 0], data.loc[y == 1, 1], marker= '+', c= 'black',
            linewidths= 3, s= 100, label= 'Admitted')

plt.scatter(data.loc[y == 0, 0], data.loc[y == 0,1], marker= 'o', c= 'yellow',
            linewidths= 1, edgecolors= 'k', s= 80, label= 'Not admitted')

# Only need 2 points to define a line, so choose two endpoints
plot_x = np.array([np.min(X[:,1])-2, np.max(X[:,2])+2])

# Calculate the decision boundary line
theta = np.array(Result[0]).reshape((X.shape[1], 1))
plot_y = np.multiply(((-1.) / theta[2]) , (np.multiply(theta[1], plot_x) + theta[0]))

# print('x =\n', plot_x)
# print('y =\n', plot_y)

# Plot, and adjust axes for better viewing
plt.plot(plot_x, plot_y, linewidth= 2, label= 'Decision Boundary')

ax.set(title= 'Training Dataset with Decision Boundary', xlabel= 'Exam 1 score', ylabel= 'Exam 2 score',
        xlim= (28, 101), ylim= (21, 101))
ax.legend(loc= 1, shadow= True)
plt.show()

# Predict and Accuracies
def predict(theta, X):
    # PREDICT Predict whether the label is 0 or 1 using learned logistic
    # regression parameters theta
    p = np.zeros((X.shape[0],1))

    for i in range(X.shape[0]):
        x = X[i, :].T
        if sigmoid(theta.T * x) >= 0.5 :
            p[i] = 1
        else:
            p[i] = 0

    return p

prob = sigmoid(np.matrix([1, 45, 85]) * theta)

print('\nFor a student with scores 45 and 85, we predict an admission probability of %.3f' %prob[0])
print('Expected value: 0.775 +/- 0.002')
print('\n**************************************')

# Compute accuracy on our training set
p = predict(theta, X)

print('Train Accuracy: %% %.1f' %(np.mean(p == y) * 100))
print('Expected accuracy (approx): % 89.0')
print('\n**************************************')


import numpy as np

# We create a 4 x 5 ndarray that contains integers from 0 to 19
X = np.arange(20).reshape(4, 5)

# We print X
print()
print('X = \n', X)
print()

# We select all the elements that are in the 2nd through
# 4th rows and in the 3rd to 5th columns

Z = X[1:4,2:5]

# We print Z
print('Z = \n', Z)

# We can select the sanem elements as above using method 2

W = X[1:, 2:]

# We print W
print()
print('W = \n', W)

# We select all the elements that are in the 1st through
# 3rd rows and in the 3rd to 4th columns

Y = X[:3, 2:5]

# We print Y
print()
print('Y = \n', Y)

# We select all the elements in the 3rd row
v = X[2,:]

# We print v
print()
print('v = ', v)

# We select all the elements in the 3rd column
q = X[:,2]

# We print q
print()
print('q = ', q)

# We select all the elements in the 3rd column but return a rank 2 ndarray
R = X[:,2:3]

# We print R
print()
print('R = \n', R)

## Copying Concept, View Stuffs

# We create a 4 x 5 ndarray that contains integers from 0 to 19
X = np.arange(20).reshape(4, 5)

# We print X
print()
print('X = \n', X)
print()

# We select all the elements that are in the 2nd through 4th rows and in the 3rd to 4th columns
Z = X[1:4,2:5]

# We print Z
print()
print('Z = \n', Z)
print()

# We change the last element in Z to 555
Z[2,2] = 555

# We print X
print()
print('X = \n', X)
print()

# We create a 4 x 5 ndarray that contains integers from 0 to 19
X = np.arange(20).reshape(4, 5)

# We print X
print()
print('X = \n', X)
print()

# create a copy of the slice using the np.copy() function
Z = np.copy(X[1:4,2:5])

#  create a copy of the slice using the copy as a method
W = X[1:4,2:5].copy()

# We change the last element in Z to 555
Z[2,2] = 555

# We change the last element in W to 444
W[2,2] = 444

# We print X
print()
print('X = \n', X)

# We print Z
print()
print('Z = \n', Z)

# We print W
print()
print('W = \n', W)

# It is often useful to use one ndarray to make slices,
# select, or change elements in another ndarray.
# Let's see some examples:

# We create a 4 x 5 ndarray that contains integers from 0 to 19
X = np.arange(20).reshape(4, 5)

# We create a rank 1 ndarray that will serve as indices to select elements from X
indices = np.array([1,3])

# We print X
print()
print('X = \n', X)
print()

# We print indices
print('indices = ', indices)
print()

# We use the indices ndarray to select the 2nd and 4th row of X
Y = X[indices,:]

# We use the indices ndarray to select the 2nd and 4th column of X
Z = X[:, indices]

# We print Y
print()
print('Y = \n', Y)

# We print Z
print()
print('Z = \n', Z)

# Selection of specific elements within ndarrays

# Diagonal

# We create a 4 x 5 ndarray that contains integers from 0 to 19
X = np.arange(25).reshape(5, 5)

# We print X
print()
print('X = \n', X)
print()

# We print the elements in the main diagonal of X
print('z =', np.diag(X))
print()

# We print the elements in the main diagonal of X
print('z =', np.diag(X, k=0))
print()

# We print the elements above the main diagonal of X
print('y =', np.diag(X, k=1))
print()

# We print the elements below the main diagonal of X
print('w = ', np.diag(X, k=-1))

# We print the elements above the main diagonal of X
print('t =', np.diag(X, k=3))
print()

# We print the elements below the main diagonal of X
print('q = ', np.diag(X, k=-3))

## Unique elements

# Create 3 x 3 ndarray with repeated values
X = np.array([[1,2,3],[5,2,8],[1,2,3]])

# We print X
print()
print('X = \n', X)
print()

# We print the unique elements of X
print('The unique elements in X are:',np.unique(X))


from sympy import latex, factor, lambdify
from utils import r

def gauss_seidel_method(x_exp, y_exp, z_exp=None, x_val=0, y_val=0, z_val=0, iter=1):
    out_str = ""
    x, y, z = symbols('x y z')

    new_values = [x_val, y_val, z_val]

    out_str += f"\\textbf{{Iteration {iter}}}\n\n"
    out_str += f"Substitute $y={r(new_values[1])}$, $z={r(new_values[2])}$\n"
    out_str += f"$$ x = {latex(factor(x_exp))} ="
    new_values[0] = float(lambdify((x, y, z), x_exp)(*new_values))
    out_str += f"{r(new_values[0])} $$\n\n"

    out_str += f"Substitute $x={r(new_values[0])}$, $z={r(new_values[2])}$\n"
    out_str += f"$$ y = {latex(factor(y_exp))} ="
    new_values[1] = float(lambdify((x, y, z), y_exp)(*new_values))
    out_str += f"{r(new_values[1])} $$\n\n"

    if z_exp:
        out_str += f"Substitute $x={r(new_values[0])}$, $y={r(new_values[1])}$\n"
        out_str += f"$$ z = {latex(factor(z_exp))} ="
        new_values[2] = float(lambdify((x, y, z), z_exp)(*new_values))
        out_str += f"{r(new_values[2])} $$\n\n"

    out_str += f"After Iteration {iter}\n"
    out_str += f"$$ (x, y, z) = ({', '.join([r(v) for v in new_values])})$$\n\n"

    if iter != 1:
        out_str += f"\\textbf{{Approximate Error}}\n\n"
        out_str += f"$$ \\epsilon_a(x) = \\frac{{\\left|\\text{{latest value of x}} - \\text{{previous value of x}}\\right|}}{{\\left|\\text{{latest value of x}}\\right|}} \\times 100 $$\n"
        out_str += f"$$ \\epsilon_a(x) = \\frac{{\\left|{r(new_values[0])} - ({r(x_val)})\\right|}}{{\\left|{r(new_values[0])}\\right|}} \\times 100 $$\n"

        e_x = abs(new_values[0] - x_val) / abs(new_values[0]) * 100
        out_str += f"$$ \\epsilon_a(x) = {r(e_x, 2)} \% $$\n"

        out_str += f"$$ \\epsilon_a(y) = \\frac{{\\left|\\text{{latest value of y}} - \\text{{previous value of y}}\\right|}}{{\\left|\\text{{latest value of y}}\\right|}} \\times 100 $$\n"
        out_str += f"$$ \\epsilon_a(y) = \\frac{{\\left|{r(new_values[1])} - ({r(y_val)})\\right|}}{{\\left|{r(new_values[1])}\\right|}} \\times 100 $$\n"

        e_y = abs(new_values[1] - y_val) / abs(new_values[1]) * 100
        out_str += f"$$ \\epsilon_a(y) = {r(e_y, 2)} \% $$\n\n"

        if z_exp:
            out_str += f"$$ \\epsilon_a(z) = \\frac{{\\left|\\text{{latest value of z}} - \\text{{previous value of z}}\\right|}}{{\\left|\\text{{latest value of z}}\\right|}} \\times 100 $$\n"
            out_str += f"$$ \\epsilon_a(z) = \\frac{{\\left|{r(new_values[2])} - ({r(z_val)})\\right|}}{{\\left|{r(new_values[2])}\\right|}} \\times 100 $$\n"

            e_z = abs(new_values[2] - z_val) / abs(new_values[2]) * 100
            out_str += f"$$ \\epsilon_a(z) = {r(e_z, 2)} \% $$\n\n"

    if iter < 5:
        x_val, y_val, z_val = new_values
        out_str += gauss_seidel_method(x_exp, y_exp, z_exp, x_val, y_val, z_val, iter+1)

    return out_str

if __name__ == '__main__':
    from sympy import solve, Eq, symbols
    x, y = symbols('x y')

    print(gauss_seidel_method(
        solve(Eq(3*x+2*y, 4), y)[0],
        solve(Eq(x - 2*y, 5), x)[0],
        # solve(Eq(-3*x -y + 7*z, -34), z)[0],
    ))


import numpy

def bisection(f, interval, eps_abs = 1e-10, max_step = 100):
    """
    Use bisection to find x such that f(x) = 0.
    """

    x_lo, x_hi = interval
    x = (x_lo + x_hi) / 2.0
    f_lo = f(x_lo)
    if (abs(f_lo) < eps_abs):
        return x_lo
    f_hi = f(x_hi)
    if (abs(f_hi) < eps_abs):
        return x_hi
    #assert(f_lo*f_hi < 0), "f(Endpoints) must change sign!"
    if f_lo*f_hi > 0:
        print("Warning! f(endpoints) have same sign!")

    f_mid = f(x)
    step = 0
    while (step < max_step) and abs(f_mid) > eps_abs:
        step += 1
        if f_lo * f_mid < 0.0:
            x_hi = x
            f_hi = f(x_hi)
        else:
            x_lo = x
            f_lo = f(x_lo)
        x = (x_lo + x_hi) / 2.0
        f_mid = f(x)

    return x

if __name__ == "__main__":
    def f(x):
        return numpy.exp(x) + x - 2
    def g(x):
        return numpy.sin(x**2) - 0.1 * x

    interval = [0, 1]
    s = bisection(f, interval)
    print("s = {}, f(s) = {}".format(s, f(s)))
    for lower in range(1,9):
        interval = [lower, 10]
        try:
            s = bisection(g, interval)
            print("interval = [{}, 10], s = {}, g(s) = {}".format(lower, s, g(s)))
        except:
            pass


import numpy as np

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

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

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

    Returns:
        (l, u, p): result

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

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

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

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

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

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

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

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

    # print(a, p)

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

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

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

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

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

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

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

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

    n = np.shape(l)[0]

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

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

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

    return x

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

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

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

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

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

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

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

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

    Returns:
        x: float, a 的逆矩阵

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

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

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

    return X


"""
Laborator 5
"""

import numpy as np
import proceduri as proc
import time

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

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

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

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

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

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

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

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

    x_old = x_new[:]

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

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

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


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

import numpy as np

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


# for decimal division
from __future__ import division

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

from cvxpy import *

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

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

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

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

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

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

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

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

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

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


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

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

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

    Args:

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

    """

    n,_m = A.shape

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

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

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

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

    if no_reduction:
        return

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

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

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

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

    Args:

        A (ndarray): input matrix

    Returns:

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

    """

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

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

    Args:

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

    Returns:

        x (ndarray): Solution vector

    """

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

    return x

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

    Args:

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

    Returns:

        x (ndarray): Solution vector

    """

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

    return x

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

    Args:

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

    Returns:

        x (ndarray): Solution vector

    """

    converged = False

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

    # b. iterate
    x = x0
    i = 0

    if do_print:
        print('  ',x)

    while i < max_iter and not converged:

        # i. save previous
        x_prev = x

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

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

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

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

        i += 1

    return x

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

    Args:

        A (ndarray): input matrix

    Returns:

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

    """

    n = len(A)

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

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

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

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

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

    return L,U

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

    Args:

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

    Returns:

        mat (sympy.matrix): Sympy Matrix

    """

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

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

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

    return mat

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

    n,m = A.shape

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

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

    # c. return filled matrix
    return f()


# Question 3, Lab 07
# AB Satyaprakash, 180123062

# imports
import numpy as np

# functions

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

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

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

# program body
# part (a)

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

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

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

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


import scipy.stats
import numpy as np

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

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

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

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

    pooled_var = var_x_norm + var_y_norm

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

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

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

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

    else:
        p_value = null_dist.sf(test_statistic)

    return test_statistic, p_value

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

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

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

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

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

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

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

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

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

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

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

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

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

    else:
        p_value = null_dist.sf(test_statistic)

    return test_statistic, p_value

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

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

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

if __name__ == "__main__":
    _test()


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

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

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

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

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

  Sigma = np.corrcoef(x1, x2)

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

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

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

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

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

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


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

"""

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

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

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

except SympifyError:
    print("Invalid input")

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

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


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

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

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

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

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


import math
import numpy as np

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

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


#
# demo_statistics.py
#

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

print('')


# coding: utf-8

# In[2]:

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

# In[5]:

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

# In[10]:

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

# In[12]:

optimize.fmin_bfgs(f, 3)

# In[13]:

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

# In[15]:

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

# In[16]:

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

# In[27]:

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

# In[28]:

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

# In[29]:

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

# In[ ]:


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

"""""""""""

import numpy as np

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

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

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

MNP = NP1@NP2

MNP3 = np.dot(NP1,NP2)

MNP2 = NP1*NP2

MNP4 = np.multiply(NP1,NP2)

Sum1 = NP1+NP2

Sub1 = NP1-NP2

Sub2 = np.subtract(NP1,NP2)

np.sum(NP1)

Broad_Nump = NP1+3

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

NP1+NP3

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

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

np.math.sqrt(10)

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

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

# Generate Float No.

np.random.rand()

# Generate Integer No.

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

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

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

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

F_Random_Ar = Random_Ar[Filter_Ar]

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

Mean_N = np.mean(Data_N)

Median_N = np.median(Data_N)

Var_N = np.var(Data_N)

SD_N = np.std(Data_N)

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

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

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


import numpy as np

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

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

    return C

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

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

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

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

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

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

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

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

        C[k] = a_k * sum
    return C

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

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

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


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

import numpy as np

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

NDIM = 4

# This is the mean of the MVN distribution.

MEAN =  [0.0,0.0,0.0,0.0]

# This is the covariance matrix of the MVN distribution.

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

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

INVCOV = np.linalg.inv(COVMAT)

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

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

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


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

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

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

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

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

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

    Parameters
    ==========

    n : the order of quadrature

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

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

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

    Returns
    =======

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

    Examples
    ========

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

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

    See Also
    ========

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

    References
    ==========

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

    return out_img, output_hist, input_hist, s

# %%

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

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