## I'll be following these two tutorials
# http://apmonitor.com/me575/index.php/Main/SimulatedAnnealing
# https://perso.crans.org/besson/publis/notebooks/Simulated_annealing_in_Python.html
# Here some important points
    # If the change in energy is negative, the energy state of the new
    # configuration is lower and the new configuration is accepted.
    # I'll be using the Boltzman factor unless Jeremy tells me something else.
    # If the change in energy is positive, the new configuration has a
    # higher energy state; however, it may still be accepted according
    #to the Boltzmann probability factor
    # The Boltzmann probability is compared to a random number drawn from a
    # uniform distribution between 0 and 1; if the random number is smaller
    # than the Boltzmann probability, the configuration is acceptedself.
    # This allows the algorithm to escape local minima.
import numpy as np
from math import sin, pi, log, exp
import matplotlib
import matplotlib.pyplot as plt
## energy: in my case it will be the original function with a minus
# just to convert the maxima into a minima (cp & pt from lm00 )

def energy(x,nn=1):
    n = nn
    # I add the minus sign to cenvert the problem in a minimization
    # problem, to feel familiar with a sytem looking for the minimum energy.
    f = (16*x[0]*(1-x[0])*x[1]*(1-x[1])*sin(n*pi*x[0])*sin(n*pi*x[1]))**2
    return -f

def acceptance_p(dE,T, dE_avg):
    B_factor = exp(-dE / (dE_avg * T) )
    return B_factor

def annealing(x_start=[0.,0.],n=100,m=20,nn=1):
    #print(x_start)
    # number of accepted solutions
    n_acc = 0.
    # Probability of acceptin worst solution at the beginning and in the end
    p_0 = 0.7
    p_n = 0.001
    # Initial and final Temperature
    T_0 = -1./log(p_0)
    T_n = -1./log(p_n)
    # Temperature reduction for each step in the walk
    T_frac = (T_n/T_0)**(1.0/(n-1.0))
    ## Numpy array to store the values for latter plotting results
    x = np.zeros((n+1,2))
    x[0] = x_start
    # Updating value during iterations
    x_i  = [0,0]
    n_aa = n_acc + 1 # I automatically accept the starting point
    # Current optimum point
    x_c = x_start # Again the starting point is initially accepted
    energy_c = energy(x_i,nn) # Again... that's why I use _i
    energies = np.zeros(n+1)
    energies[0] = energy_c # Again... that's why I use _i
    # Current temperature
    T = T_0
    # k_b equivalent
    dE_avg = 0.0
    for i in range(n):
        for j in range(m):
            # Generating new point
            x_i[0] = x_c[0] + np.random.random() - 0.5
            x_i[1] = x_c[1] + np.random.random() - 0.5
            aux = np.array(x_i)
            # Making sure the new point is in the unit sqare with np.clip()
            x_i[0] = np.clip(aux,0.,1.)[0]
            x_i[1] = np.clip(aux,0.,1.)[1]
            # Change in energy
            dE = abs(energy(x_i,nn)-energy_c)
            if  energy(x_i,nn) < energy_c:
                x_c[0], x_c[1] = x_i[0], x_i[1]
                energy_c = energy(x_i,nn)
                n_acc = n_acc + 1.
                dE_avg = (dE_avg*(n_acc-1) + dE)/n_acc
            else:
                if (i==0) and (j==0):
                    dE_avg = dE
                p = acceptance_p(dE,T,dE_avg)
                if np.random.random() < p:
                    x_c[0], x_c[1] = x_i[0], x_i[1]
                    energy_c = energy(x_i,nn)
                    n_acc = n_acc + 1.
                    dE_avg = (dE_avg*(n_acc-1) + dE)/n_acc
        # Best values after a thermalization
        x[i+1][0]     = x_c[0]
        x[i+1][1]     = x_c[1]
        energies[i+1] = energy_c
        # Reducing the temperature
        T = T_frac * T
    return energies,x

def annealing_plot(points, energies, x_s, nn,chain):
    #opt = 0.5*np.ones(energies.size)
    plt.figure()
    plt.suptitle('x and y starting at ' + x_s + \
    ' for n = ' + str(nn))
    plt.subplot(121)
    plt.ylim(-0.1,1.1)
    plt.hlines(0.5,0,energies.size)
    plt.xlabel('chain length')
    plt.ylabel('x')
    plt.plot(points[:,0],'r.')
    plt.xticks(np.arange(0,110,20))
    #plt.plot(points[:,0],opt, 'r')
    #plt.title("x")
    plt.subplot(122)
    plt.ylim(-0.1,1.1)
    plt.hlines(0.5,0,energies.size)
    plt.ylabel('y')
    plt.xlabel('chain length')
    plt.plot(points[:,1],'b.')
    plt.xticks(np.arange(0,120,20))
    #plt.plot(points[:,1],opt, 'b')
    #plt.title("y")
    plt.savefig('xy_evolution_n_'+ str(nn) + '_chain_' + str(chain) + '.png')
    plt.close()

    plt.figure()
    plt.title("Energy for n = " + str(nn) + ' starting at ' + x_s)
    plt.ylim(-0.1,1.1)
    plt.hlines(1.,0,energies.size)
    plt.xlabel('chain length')
    plt.ylabel('f(x,y)')
    plt.plot(-1.*energies,'b.')
    plt.xticks(np.arange(0,110,10))
    plt.savefig('energy_n_' + str(nn)+ '_chain_' + str(chain) +'.png')
    plt.close()

## Gelman-Runin Statistics

class GR:
    def __init__(self,data):
        self.data = data
        self.length = data.shape[1]

    def mean_var(self):
        # Saving the mean and variance for each chain
        # Using only the last third of data: the idea is to use only data that is 'burned'
        idx = self.data.shape[0]*2//3
        data = self.data[idx:]
        means = np.mean(data, axis=0)
        variances = np.var(data, axis=0)
        return means, variances

    def GelmanRubin(self):
        means, variances = self.mean_var()
        N = self.length
        M = means.shape[0]
        ovl_mean = np.mean(means)
        # Between-chains variances
        B = (means - ovl_mean)**2
        B = (N/(M-1))*B.sum()
        # Between-chains variances
        W = (1/M)*variances.sum()
        # Pooled variance
        V = ((N-1)/N)*W + ((M+1)/(M*N))*B
        # Potential Scale Reduction Factor
        PSRF = V/W
        return PSRF,B, W

# def mean_var(data):
#     # Saving the mean and variance for each chain
#     # Using only the last third of data: the idea is to use only data that is 'burned'
#     idx = data.shape[0]*2//3
#     data = data[idx:]
#     means = np.mean(data, axis=0)
#     variances = np.var(data, axis=0)
#     return means, variances
#
# def GelmanRubin(means, variances,length):
#     N = length
#     M = means.shape[0]
#     ovl_mean = np.mean(means)
#     # Between-chains variances
#     B = (means - ovl_mean)**2
#     B = (N/(M-1))*B.sum()
#     # Between-chains variances
#     W = (1/M)*variances.sum()
#     # Pooled variance
#     V = ((N-1)/N)*W + ((M+1)/(M*N))*B
#     # Potential Scale Reduction Factor
#     PSRF = V/W
#     return PSRF,B, W


import math
from scipy.optimize import fsolve

class Compression():

	def __init__(self, M,theta,gamma=1.4):
		self.M = M
		self.theta = math.radians(theta)
		self.gamma = gamma

	def getWaveAngle(self, M):
		'''
		Returns wave angle (beta).
		Solves equation 9.23 from the book for beta.
		'''
		gamma = self.gamma
		theta = self.theta
		func = lambda beta: ((2*(1/math.tan(beta)) * (M**2 * math.sin(beta)**2 -1))/\
							(M**2*(gamma+math.cos(2*beta)) + 2)) - math.tan(theta)
		beta = fsolve(func, .707)
		beta = min(beta) # weak solution
		return beta

	def getMachNormal(self, M):
		beta = self.getWaveAngle(M)
		M_normal = M*math.sin(beta)
		return M_normal

	def getMach2(self,M,theta,gamma):
		'''
		Returns Mach number after Oblique shock.
		M 		= the initial Mach number before the wedge.
		theta	= the wedge angle from horizontal.
		gamma 	= the ratio of specific heats. (1.4 @ air at sea level conditions)
		'''
		M_normal = self.getMachNormal(M)
		M_normal2 = math.sqrt(\
			(1+((gamma-1)/2)*M_normal**2)/\
			(gamma*M_normal**2 - (gamma-1)/2))

		beta = self.getWaveAngle(M)
		M_2 = M_normal2/(math.sin(beta - theta))
		return M_2

	def getPropertyRatios(self,M,gamma):
		'''
		Returns ratios for density (rho), pressure, and temperature
		as tuple in the respective order.

		Ratios are equivalent to:

		 (final property value)
		-------------------------
		 (initial property value)
		'''
		M_normal = self.getMachNormal(M)

		rho_ratio = (gamma+1)*M_normal**2/\
					(2+(gamma-1)*M_normal**2)
		pressure_ratio = 1 + (2*gamma*(M_normal**2 - 1)/\
						(gamma+1))
		temp_ratio = pressure_ratio/rho_ratio

		return(rho_ratio,pressure_ratio,temp_ratio)

	def getValues(self):
		M_2 = self.getMach2(self.M,self.theta,self.gamma)
		rho_ratio,pressure_ratio,temp_ratio = self.getPropertyRatios(self.M,self.gamma)
		return (M_2,rho_ratio,pressure_ratio,temp_ratio)

class Expansion():

	def __init__(self,M,theta,gamma = 1.4):
		self.M = M
		self.theta = math.radians(theta)
		self.gamma = gamma

	def getPrantlMeyer(self,M,gamma):
		vM = math.sqrt((gamma+1)/(gamma-1))*\
			math.atan(math.sqrt((gamma-1)*(M**2 - 1)/(gamma+1))) \
			- math.atan(math.sqrt(M**2 - 1))
		return vM

	def getInversePrantlMeyer(self,vM):
		'''
		Returns Mach number from from Prantl-Meyer function output v(M).
		Uses the Hall approximation for the inverse of the Prantl-Meyer function:
		http://www.pdas.com/pm.pdf

		The maximum error of Hall approximation for gamma = 1.4 is less than 1%.
		'''
		# Approximation coefficients for gamma  = 1.4:
		A,B,C,D,E = (1.3604,0.0962,-0.5127,-0.6722,-0.3278)
		# Input angle (vM) over the maximum turning angle.
		x = (vM / (math.pi * 0.5 * (math.sqrt(6) - 1)))**(2/3)
		# Hall approximation:
		M = (1 + A*x + B*x**2 + C*x**3) /\
			(1 + D*x + E*x**2)
		return M

	def getMach2(self,M,theta,gamma):
		vM1 = self.getPrantlMeyer(M,gamma)
		vM2 = theta + vM1
		M2 = self.getInversePrantlMeyer(vM2)
		return M2

	def getPropertyRatios(self,M2,M1,gamma):
		'''
		Returns ratios for density (rho), pressure, and temperature
		as tuple in the respective order.

		Ratios are equivalent to:

		 (final property value)
		-------------------------
		 (initial property value)
		'''
		temp_ratio = (1+((gamma-1)/2)*M1**2)/\
					(1+((gamma-1)/2)*M2**2)
		pressure_ratio = ((1+((gamma-1)/2)*M1**2)/\
					(1+((gamma-1)/2)*M2**2))**(gamma/(gamma-1))
		rho_ratio = pressure_ratio/temp_ratio
		return(rho_ratio,pressure_ratio,temp_ratio)

	def getValues(self):
		M_2 = self.getMach2(self.M,self.theta,self.gamma)
		rho_ratio,pressure_ratio,temp_ratio \
            = self.getPropertyRatios(M_2,self.M,self.gamma)
		return (M_2,rho_ratio,pressure_ratio,temp_ratio)

def main():

	# Test compression, should be M2 = 1.21,p2 = 2.82, T2 = 399.7
	M1,p1,T1 = (2.0,1.0,288.0) #Mach number, atm, K
	theta = 20 # degrees
	M2,rho_ratio,pressure_ratio,temp_ratio = Compression(M1,theta).getValues()
	p2 = p1*pressure_ratio
	T2 = T1*temp_ratio

	print('Compression:')
	print(M2,p2,T2)

	# Test expansion, should be M2 = 2.0, p2 = 0.469, T2 = 232
	M1,p1,T1 = (1.5,1.0,288.0) #Mach number, atm, K
	theta = 15 # degrees
	M2,rho_ratio,pressure_ratio,temp_ratio = Expansion(M1,theta).getValues()
	p2 = p1*pressure_ratio
	T2 = T1*temp_ratio

	print('Expansion:')
	print(M2,p2,T2)

if __name__ == '__main__':
	main()


import numpy as np

# ---------------------------------------
# General helper methods such as easily generate a vector or identity matrix

# Get dimension of vector or matrix (assuming matrix is square)
def np_dim(np_obj):
    return np_obj.shape[0]

# Easy syntax for generating 2D or 3D vectors
def vect(x=0, y=0, z=None, scale=1):
    if z is None:
        return np.array((x, y)) * scale
    return np.array((x, y, z)) * scale

# Find length of vector
# Default power = 2 has 'unit circle' as an actual circle, e.g. Euclidean geometry / Pythagoras' theorem
# Other powers give different shapes of unit circle,
# e.g. power = 0.5 gives a 4 pointed star, 1 gives a diamond, 4 for squircle, 10 is nearly a square
def vect_len(vect, power=2):
    return np.sum(abs(np.float_power(abs(vect), power))) ** (1 / power)

# Find a metric for matrix using length of transformation of x-coord (1, 0)
# Currently 2D only
def metric_matrix_x_coord(mx):
    return (mx[0][0] ** 2 + mx[1][0] ** 2) ** 0.5

# Find a metric for matrix using square root of sum of squares of matrix entries
# Identity matrix in 2D or 3D has metric 1
def metric_matrix_rms(mx):
    return np.sum(mx * mx * (1 / np_dim(mx))) ** 0.5

# Construct metric for matrix using absolute values of the eigenvalues
# Identity matrix in 2D or 3D has metric 1
# Metric using minimum
def metric_matrix_min_eig_val(mx):
    return min(abs(np.linalg.eig(mx)[0]))

# Metric using maximum
def metric_matrix_max_eig_val(mx):
    return max(abs(np.linalg.eig(mx)[0]))

# Metric using ratio of max/min
def metric_matrix_ratio_eig_val(mx):
    result = abs(np.linalg.eig(mx)[0])  # Save the intermediate calculation
    return max(result) / min(result)

# Angle calculator for matrices in O(2) (symmetries of a 2D circle)
# Find angle of vect(1, 0) under transformation by mx
# Returns value between -90 and 270
def mx_angle(mx):
    x = mx[0][0]
    y = mx[0][1]
    deg_from_sign = 180 if x < 0 else 0
    deg_from_angle = 90 if y == 0 else np.degrees(np.arctan(x / y))
    return deg_from_sign + deg_from_angle

# Easy syntax for generating matrix identity
# Defaults to 2D identity matrix
def mx_id(dim=2):
    return np.identity(dim)

# Scale matrix, which is mx_id * scale
# Defaults to 2D identity matrix
def mx_scale(scale=1, dim=2):
    return mx_id(dim) * scale

# Diagonal matrix in 2D or 3D
# Defaults to 2D identity matrix
def mx_diag(x=1, y=1, z=None):
    if z is None:
        return np.diag((x, y))
    return np.diag((x, y, z))

# ---------------------------------------
# Generate rotation matrices in 2D or 3D

# 2D clockwise rotation matrix in XY plane, angle in degrees, +1° rotates Y towards X, with optional reflection
def mx_rotd(angle=0, scale=1, reflect=False):
    angle_in_radians = np.radians(angle)
    c, s = np.cos(angle_in_radians), np.sin(angle_in_radians)
    # TODO: document which plane this is reflecting in
    r = 1
    if reflect:
        r = -1
    return np.array(((c, s), (-s * r, c * r))) * scale

# 3D clockwise rotation matrix in XY plane, angle in degrees, +1° rotates Y towards X
def mx_rotd_XY(angle=0, scale=1):
    angle_in_radians = np.radians(angle)
    c, s = np.cos(angle_in_radians), np.sin(angle_in_radians)
    return np.array(((c, s, 0), (-s, c, 0), (0, 0, 1))) * scale

# 3D clockwise rotation matrix in XZ plane, angle in degrees, +1° rotates Z towards X
def mx_rotd_XZ(angle=0, scale=1):
    angle_in_radians = np.radians(angle)
    c, s = np.cos(angle_in_radians), np.sin(angle_in_radians)
    return np.array(((c, 0, s), (0, 1, 0), (-s, 0, c))) * scale

# 3D clockwise rotation matrix in YZ plane, angle in degrees, +1° rotates Z towards Y
def mx_rotd_YZ(angle=0, scale=1):
    angle_in_radians = np.radians(angle)
    c, s = np.cos(angle_in_radians), np.sin(angle_in_radians)
    return np.array(((1, 0, 0), (0, c, s), (0, -s, c))) * scale

# Utility method to do an XY rotation, then a XZ rotation, then an optional scale
# Applying a matrix transformation is _pre_multiplication, so XZ goes first.
# Scaling is commutative, so can go anywhere.
# Angles are in degrees
def mx_rotd_3D(ang_xy=0, ang_xz=0, scale=1):
    return mx_rotd_XZ(ang_xz) @ mx_rotd_XY(ang_xy, scale)

# ---------------------------------------
# Generate reflection matrices in 2D or 3D

# Reflection matrix in x-axis
def mx_refl_X(dim=2, scale=1):
    if dim == 3:
        return np.array(((-1, 0, 0), (0, 1, 0), (0, 0, 1))) * scale
    return np.array(((-1, 0), (0, 1))) * scale

# Reflection matrix in y-axis
def mx_refl_Y(dim=2, scale=1):
    if dim == 3:
        return np.array(((1, 0, 0), (0, -1, 0), (0, 0, 1))) * scale
    return np.array(((1, 0), (0, -1))) * scale

# Reflection matrix in z-axis
def mx_refl_Z(scale=1):
    return np.array(((1, 0, 0), (0, 1, 0), (0, 0, -1))) * scale

# ---------------------------------------
# Discrete symmetry groups

# Dihedral group of regular polygon 2, 3, 4, 5, 6...
# (rectangle, triangle, square, pentagon, hexagon...)
# where bottom edge is horizontal (so horizontal reflection)
# dh(5, 1) for identity
# dh(5, 2) for 72 degree rotation anticlockwise
# dh(5, 6) for horizontal reflection
# dh(8, 6) for 225 (45 * 5) degree rotation anticlockwise
def mx_dh(sides=3, num=1, scale=1):
    if not isinstance(sides, int):
        raise TypeError("Number of sides must be an integer")
    if sides < 2:
        raise TypeError("Number of sides must be at least 2")
    if not isinstance(num, int):
        raise TypeError("Transformation number must be an integer")
    if num < 1:
        raise TypeError("Transformation number must be at least 1")
    if num > sides * 2:
        raise TypeError(
            f"Transformation number must be at most {sides * 2}")
    rotation_num = (num - 1) % sides  # 0, 1, 2, ..., sides - 1
    reflection_num = ((num - 1) - rotation_num) / sides  # 0, 1
    rotation_mx = mx_rotd((-360 / sides) * rotation_num)
    refl_x_mx = np.array((((-1) ** reflection_num, 0), (0, 1)))
    result = rotation_mx @ refl_x_mx
    return result * scale

# Square transformations 1 to 8
# sq(1) for identity
# sq(2) for 90 degree rotation anticlockwise
# sq(5) for *vertical* reflection (should have been horizontal... but already published on Nifty Ink)
def mx_sq(num=1, scale=1):
    if not isinstance(num, int):
        raise TypeError("Argument is not integer")
    if num < 1:
        raise TypeError("Argument must be at least 1")
    if num > 8:
        raise TypeError("Argument must be at most 8")
    rotation_num = (num - 1) % 4  # 0, 1, 2, 3
    reflection_num = ((num - 1) - rotation_num) / 4  # 0, 1
    rotation_mx = mx_rotd((-90) * rotation_num)
    refl_y_mx = np.array(((1, 0), (0, (-1) ** reflection_num)))
    result = rotation_mx @ refl_y_mx
    return result * scale


import numpy as np

def cartesian2spherical(cartesian):
    """Convert cartesian to spherical coordinates

    Args:
        cartesian (ndarray): Vector (x, y, z)

    Returns:
        spherical (ndarray): Vector (distance, azimuth, elevation)
    """
    cartesian = np.array(cartesian).squeeze()
    x, y, z = cartesian
    distance = np.linalg.norm(cartesian)
    azimuth = np.arccos(z / distance)
    elevation = np.arctan2(y, x)  # Use arctan2 instead of arctan to get proper sign!
    return np.array([distance, azimuth, elevation])

def spherical2cartesian(spherical):
    """Convert spherical to cartesian coordinates

    Args:
        spherical (ndarray): Vector (distance, azimuth, elevation)

    Returns:
        cartesian (ndarray): Vector (x, y, z)
    """
    spherical = np.array(spherical).squeeze()
    distance, azimuth, elevation = spherical
    x = distance * np.sin(azimuth) * np.cos(elevation)
    y = distance * np.sin(azimuth) * np.sin(elevation)
    z = distance * np.cos(azimuth)
    return np.array([x, y, z])

def cartesian2polar(cartesian):
    """Converts cartesian to polar coordinates

    Args:
        cartesian (ndarray): Vector (x, y)

    Returns:
        polar (ndarray): Vector (range, azimuth)
    """
    cartesian = np.array(cartesian).squeeze()
    x, y = cartesian
    r = np.linalg.norm([x, y])
    azimuth = np.arctan2(y, x)
    return np.array([r, azimuth])

def polar2cartesian(polar):
    """Converts polar to cartesian coordinates

    Args:
        polar (ndarray): Vector (range, azimuth)

    Returns:
        cartesian (ndarray): Vector (x, y)
    """
    polar = np.array(polar).squeeze()
    r, azimuth = polar
    x = r * np.cos(azimuth)
    y = r * np.sin(azimuth)
    return np.array([x, y])

def random_uniform_within_circle():
    """Sample random point within a unit circle

    Returns:
        sample (ndarray): Vector (x, y)

    """
    rho = np.sqrt(np.random.uniform(0, 1))
    phi = np.random.uniform(0, 2 * np.pi)
    x = rho * np.cos(phi)
    y = rho * np.sin(phi)
    return np.array([x, y])


import numpy as np

# Gradient descent based linear regression from scratch
def compute_prediction(X, weights):
    """ Compute the prediction y_hat based on current weights
    Args:
        X (numpy.ndarray)
        weights (numpy.ndarray)
    Returns:
        numpy.ndarray, y_hat of X under weights
    """
    predictions = np.dot(X, weights)
    return predictions

def update_weights_gd(X_train, y_train, weights, learning_rate):
    """ Update weights by one step
    Args:
        X_train, y_train (numpy.ndarray, training data set)
        weights (numpy.ndarray)
        learning_rate (float)
    Returns:
        numpy.ndarray, updated weights
    """
    predictions = compute_prediction(X_train, weights)
    weights_delta = np.dot(X_train.T, y_train - predictions)
    m = y_train.shape[0]
    weights += learning_rate / float(m) * weights_delta
    return weights

def compute_cost(X, y, weights):
    """ Compute the cost J(w)
    Args:
        X, y (numpy.ndarray, data set)
        weights (numpy.ndarray)
    Returns:
        float
    """
    predictions = compute_prediction(X, weights)
    cost = np.mean((predictions - y) ** 2 / 2.0)
    return cost

def train_linear_regression(X_train, y_train, max_iter, learning_rate, fit_intercept=False):
    """ Train a linear regression model with gradient descent
    Args:
        X_train, y_train (numpy.ndarray, training data set)
        max_iter (int, number of iterations)
        learning_rate (float)
        fit_intercept (bool, with an intercept w0 or not)
    Returns:
        numpy.ndarray, learned weights
    """
    if fit_intercept:
        intercept = np.ones((X_train.shape[0], 1))
        X_train = np.hstack((intercept, X_train))
    weights = np.zeros(X_train.shape[1])
    for iteration in range(max_iter):
        weights = update_weights_gd(X_train, y_train, weights, learning_rate)
        # Check the cost for every 100 (for example) iterations
        if iteration % 100 == 0:
            print(compute_cost(X_train, y_train, weights))
    return weights

def predict(X, weights):
    if X.shape[1] == weights.shape[0] - 1:
        intercept = np.ones((X.shape[0], 1))
        X = np.hstack((intercept, X))
    return compute_prediction(X, weights)

# A small example
X_train = np.array([[6], [2], [3], [4], [1], [5], [2], [6], [4], [7]])

y_train = np.array([5.5, 1.6, 2.2, 3.7, 0.8, 5.2, 1.5, 5.3, 4.4, 6.8])

weights = train_linear_regression(X_train, y_train, max_iter=100, learning_rate=0.01, fit_intercept=True)

X_test = np.array([[1.3], [3.5], [5.2], [2.8]])

predictions = predict(X_test, weights)

import matplotlib.pyplot as plt
plt.scatter(X_train[:, 0], y_train, marker='o', c='b')
plt.scatter(X_test[:, 0], predictions, marker='*', c='k')
plt.xlabel('x')
plt.ylabel('y')
plt.show()

# The diabetes example
from sklearn import datasets
diabetes = datasets.load_diabetes()
print(diabetes.data.shape)

num_test = 30    # the last 30 samples as testing set
X_train = diabetes.data[:-num_test, :]
y_train = diabetes.target[:-num_test]

weights = train_linear_regression(X_train, y_train, max_iter=5000, learning_rate=1, fit_intercept=True)

X_test = diabetes.data[-num_test:, :]
y_test = diabetes.target[-num_test:]

predictions = predict(X_test, weights)

print(predictions)
print(y_test)

# Directly use SGDRegressor from scikit-learn
from sklearn.linear_model import SGDRegressor
regressor = SGDRegressor(loss='squared_loss', penalty='l2', alpha=0.0001, learning_rate='constant', eta0=0.01, n_iter=1000)
regressor.fit(X_train, y_train)
predictions = regressor.predict(X_test)
print(predictions)
print(regressor.score(X_test, y_test))

# Measuring model performance after hyperparameter tuning with grid search
diabetes = datasets.load_diabetes()
num_test = 30    # the last 30 samples as testing set
X_train = diabetes.data[:-num_test, :]
y_train = diabetes.target[:-num_test]
X_test = diabetes.data[-num_test:, :]
y_test = diabetes.target[-num_test:]

param_grid = {
    "alpha": [1e-07, 1e-06, 1e-05],
    "penalty": [None, "l2"],
    "eta0": [0.001, 0.005, 0.01],
    "n_iter": [300, 1000, 3000]
}

from sklearn.model_selection import GridSearchCV
regressor = SGDRegressor(loss='squared_loss', learning_rate='constant')
grid_search = GridSearchCV(regressor, param_grid, cv=3)
grid_search.fit(X_train, y_train)

print(grid_search.best_params_)
regressor_best = grid_search.best_estimator_
# regressor_best.score(X_test, y_test)

predictions = regressor_best.predict(X_test)

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
mean_squared_error(y_test, predictions)
mean_absolute_error(y_test, predictions)
r2_score(y_test, predictions)


import numpy as np
from scipy.linalg import solve
def gaussseidel(A, B):
    row, col = np.shape(A)
    if row == col:
        n = 10000
        x = B/(np.diagonal(A))
        inbuilt = solve(A,B)
        for i in range(1, n):
            x_new = np.zeros_like(x)
            print(x)
            for i in range(row):
                x_new[i] = (B[i] - np.dot(A[i, :i], x_new[:i]) - np.dot(A[i, i + 1 :], x[i + 1 :])) / A[i, i]
            if np.allclose(x, x_new, rtol=1e-8):
                break
            else:
                x = x_new
        print("Solution: ",x)
        error = np.dot(A, x) - B
        print("Inbuilt Solution",solve(A,B))
        print("Error: ",error)
    else:
        print("Matrix is not square matrix")

if __name__ == "__main__":
    A = np.array([[8, 3, -3], [-2, -8, 5], [3, 5, 10]])
    # initialize the RHS vector
    B = np.array([14,5,-8])
    # Find diagonal coefficients
    diag = np.diag(np.abs(A))
    # Find row sum without diagonal
    off_diag = np.sum(np.abs(A), axis=1) - diag
    if np.all(diag > off_diag):
        print('matrix is diagonally dominant')
    else:
        print('NOT diagonally dominant')
    gaussseidel(A,B)


'''
Statistical Inference Through Python Codes
'''
import os
os.chdir("MyPath/Python/Chapter_05")
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from scipy.stats import binom,poisson,norm,t
import scipy.stats as st

# Binomial Likelihood
n = 10; x = 7
p_seq = np.arange(0,1.01,0.01)
lik_p = binom.pmf(x,n,p_seq)
plt.subplot(1,3,1)
plt.plot(p_seq,lik_p)
plt.xlabel("p")
plt.title("Binomial Likelihood Function")
# Poisson Likelihood
xp = [1,2,2,1,0,2,3,1,2,4]
lam_seq = np.arange(0,5.1,0.1)
plt.subplot(1,3,2)
plt.plot(lam_seq,poisson.pmf(sum(xp),len(xp)*lam_seq))
plt.xlabel('$\lambda$')
plt.title("Poisson Likelihood Function")
# Normal likelihood
n = 25
nx = np.random.normal(10,2,n)
mu_seq = np.arange(9,11.05,0.05)
plt.subplot(1,3,3)
plt.plot(mu_seq,norm(np.mean(nx),mu_seq,2))
plt.xlabel('$\mu$')
plt.title("Normal Likelihood Function")
plt.show()

# Confidence Intervals

def binom_CI(x,n,alpha):
	phat = x/n
	ll=phat-norm.ppf(1-alpha/2)*np.sqrt(phat*(1-phat)/n)
	ul=phat+norm.ppf(1-alpha/2)*np.sqrt(phat*(1-phat)/n)
	print("Binomial Lower Confidence Limit: ",max(0,ll))
	print("Binomial Upper Confidence Limit: ",min(1,ul))
binom_CI(x=7,n=10,alpha=0.01)

def normal_CI_ksd(x,sigma,alpha):
	xbar = np.mean(x)
	n = len(x)
	ll = xbar - norm.ppf(1-alpha/2)*sigma/np.sqrt(n)
	ul = xbar + norm.ppf(1-alpha/2)*sigma/np.sqrt(n)
	print("Normal Lower Confidence Limit: ",ll)
	print("Normal Upper Confidence Limit: ",ul)
n=25
nx = np.random.normal(10,2,n)
normal_CI_ksd(nx,2,0.05)

def normal_CI_uksd(x,alpha):
	xbar = np.mean(x); n = len(x); xsd = np.std(x)*np.sqrt(n/(n-1))
	ll = xbar - t.ppf(1-alpha/2,n-1)*xsd/np.sqrt(n)
	ul = xbar + t.ppf(1-alpha/2,n-1)*xsd/np.sqrt(n)
	print("Normal Lower Confidence Limit: ",ll)
	print("Normal Upper Confidence Limit: ",ul)
nx = np.random.normal(10,2,n)
normal_CI_uksd(nx,0.05)

'''
Hypothesis Testing in Python
'''
# Binomial Test
n_lcd = 893; x_lcd = 39; p_lcd = 0.04
from scipy.stats import binom_test
binom_test(x=x_lcd,n=n_lcd,p=p_lcd)
# Proportion Test
Male_Admit = [512, 353, 120, 138,  53,  22]
Male_Applied = [825, 560, 325, 417, 191, 373]
Female_Prop = np.array([0.824, 0.680, 0.341, 0.349, 0.239, 0.070])
from statsmodels.stats.proportion import proportions_chisquare
proportions_chisquare(count = Male_Admit, nobs=Male_Applied,value=Female_Prop)

# Normal test: One sample Z-test
def z_test(x,mu,sd,alternative):
	import scipy
	xmean = x.mean(); n = len(x)
	ztest = np.sqrt(n)*(xmean-mu)/sd
	if alternative=="less":
		pvalue = scipy.stats.norm.cdf(ztest)
	if alternative=="greater":
		pvalue = 1-scipy.stats.norm.cdf(ztest)
	if alternative=="two.sided":
		pvalue = 2*(1-scipy.stats.norm.cdf(ztest))
	print("The Z-test Value is :",ztest)
	print("The p-value is :",pvalue)

pH_Data = np.array([8.30,8.42,8.44,8.32,8.43,8.41,8.42,8.46,8.37,8.42])
z_test(x=pH_Data,mu=8.4,sd=0.05,alternative="less")
LCD_Data = np.array([13.37, 10.96, 12.06, 13.82, 12.96, 10.47,10.55,
16.28, 12.94, 11.43, 14.51, 12.63, 13.50, 11.50, 12.87])
z_test(x=LCD_Data,mu =12, sd =2,alternative="greater")
peanuts = np.array([8.08, 7.71, 7.89, 7.72, 8.00, 7.90, 7.77, 7.81,
8.33, 7.67, 7.79, 7.79, 7.94, 7.84, 8.17, 7.87])
z_test(x=peanuts,mu=8,sd=0.03,alternative="two.sided")

# Normal test: Two sample Z-test
def z_test_two_samples(x1,x2,s1,s2,alternative,delta):
	import scipy
	x1mean = x1.mean(); n1 = len(x1)
	x2mean = x2.mean(); n2 = len(x2)
	pooled_var = np.sqrt(s1**2/n1+s2**2/n2)
	ztest = (x1mean-x2mean-delta)/pooled_var
	if alternative=="less":
		pvalue = scipy.stats.norm.cdf(ztest)
	if alternative=="greater":
		pvalue = 1-scipy.stats.norm.cdf(ztest)
	if alternative=="two.sided":
		pvalue = 2*(1-scipy.stats.norm.cdf(ztest))
	print("The Z-test Value is :",ztest)
	print("The p-value is :",pvalue)
	print("The pooled variance is:",pooled_var)

pH_Data = np.array([8.30, 8.42, 8.44, 8.32, 8.43, 8.41, 8.42,  8.46, 8.37, 8.42])
pH_New = np.array([8.78, 8.85, 8.74, 8.83, 8.82, 8.79, 8.82, 8.74, 8.84, 8.78, 8.75, 8.81])
z_test_two_samples(x1=pH_Data,x2=pH_New,s1=0.05,s2=0.05,alternative="less",delta=0)

length_M1 = np.array([122.4, 123.12, 122.51, 123.12, 122.55,
               121.76, 122.31, 123.2, 122.48, 121.96])
length_M2 = np.array([122.36, 121.88, 122.2, 122.88, 123.43,
               122.4, 122.12, 121.78, 122.85, 123.04])
z_test_two_samples(x1=length_M1,x2=length_M2,s1=0.5,s2=0.5,alternative="two.sided",delta=0)

import numpy,import scipy,from scipy,from statsmodels


# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Defines quadratic loss functions."""

import jax
import jax.numpy as jnp

HIGHEST = jax.lax.Precision.HIGHEST

def quadform(hess, x, precision):
  """Computes a quadratic form (x^T @ H @ x)."""
  u = jnp.dot(hess, x, precision=precision)  # u = Hx
  return jnp.inner(x, u, precision=precision)

def loguniform(n, lambda_min, lambda_max, precision=HIGHEST):
  """Quadratic loss function with loguniform eigenvalues.

  The loss is: f(x) = (1/2) x^T H x + x^T v + b.

  The eigenvalues of the Hessian (H) are sampled uniformly on a
  logarithmic grid from lambda_min to lambda_max.

  Args:
    n: int, Problem dimension (number of parameters).
    lambda_min: float, Minimum eigenvalue of the Hessian.
    lambda_max: float, Maximum eigenvalue of the Hessian.
    precision: Which lax precision to use (default: HIGHEST).

  Returns:
    problem_fun: Function that takes a jax PRNGkey and a precision argument
      and returns an (initial_params, loss_fun) tuple.
  """

  def problem_fun(key):
    """Builds a quadratic loss problem."""
    pkey, ekey, qkey, vkey = jax.random.split(key, 4)

    # Sample eigenvalues.
    log_eigenvalues = jax.random.uniform(ekey,
                                         shape=(n,),
                                         minval=lambda_min,
                                         maxval=lambda_max)
    eigenvalues = 10**log_eigenvalues

    # Build orthonormal basis.
    basis = jax.nn.initializers.orthogonal()(qkey, shape=(n, n))

    # Define hessian.
    hess = jnp.dot(jnp.dot(basis, jnp.diag(eigenvalues), precision=precision),
                   basis.T,
                   precision=precision)

    # Random vector for the linear term in the loss.
    v = jax.random.normal(vkey, shape=(n,))

    # Compute an offset such that the global minimum has a loss of zero.
    xstar = jnp.linalg.solve(hess, -v)
    offset = -0.5 * quadform(hess, xstar, precision=precision) - jnp.inner(v, xstar, precision=precision)  # pylint: disable=line-too-long

    def loss_fun(x, _):
      return 0.5 * quadform(hess, x, precision=precision) + jnp.inner(v, x, precision=precision) + offset  # pylint: disable=line-too-long

    x_init = jax.random.normal(pkey, shape=(n,))
    return x_init, loss_fun

  return problem_fun


#!/usr/bin/env
# -*- coding: utf-8 -*-
# Copyright (C) Victor M. Mendiola Lau - All Rights Reserved
# Unauthorized copying of this file, via any medium is strictly prohibited
# Proprietary and confidential
# Written by Victor M. Mendiola Lau <ryuzakyl@gmail.com>, May 2017

from math import log2

import numpy as np

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

def calc_ent(dm):
    """Computes the entropy of a gallery

    Args:
        dm (ndarray): The similarity matrix of the gallery.

    Returns:
        The entropy value.

    Examples:
        >>> dm = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
        >>> calc_ent(dm)
        -31.546487678572873
        >>> dm = np.array([[1.0, 0.8, 0.7], [0.8, 1.0, 0.7], [0.7, 0.8, 1.0]])
        >>> calc_ent(dm)
        0.58462931767437532

    """

    # validating the received matrix
    if not isinstance(dm, np.ndarray):
        raise ValueError('NumPy ndarray expected.')

    # making a copy of the data matrix
    dm_cpy = dm.copy()

    # computing the amount of positive (>0) values
    dm_pos_mask = (dm_cpy > 0.0).astype(int)
    n = np.sum(dm_pos_mask)

    # validating that the count of positive similarity values
    if n <= 1:
        return 0.0

    # log2 of matrix values (except zero values)
    dm_cpy[dm_cpy > 0] = np.log2(dm_cpy[dm_cpy > 0])

    # normalizing dm values by log2(n)
    log_dm = dm_cpy / log2(n)

    # multiplying each number by it's log2 value (excluding non-positive similarity values)
    t = dm_pos_mask * (-dm) * log_dm

    # returning the sum of all values
    return np.sum(t)


#!/usr/bin/python
#
# Solution to exercise 1.2 from the book _Essential Algorithms_.

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

# Orders of growth. For some reason, matplotlib is happy when these are defined
# as functions, but not happy if I try to give it lambdas?
def inv_log(s):
    return 2 ** s

def inv_sqrt(s):
    return s ** 2

def inv_linear(s):
    return s

def inv_quad(s):
    return math.sqrt(s)

def inv_exp(s):
    return math.log(s, 2)

def inv_fact(s):
    n = 1
    while s > math.factorial(n):
        n += 1
    return n

def make_table():
    # number of algorithm 'steps' per second
    rate = 1E6

    # run-times in seconds: 1 sec, 1 min, 1 hour, 1 day, 1 week, 1 year
    periods = [1, 60, 3600, 86400, 604800, 31536000]

    # orders of growth: log n, sqrt n, n, n^2, 2^n, n!
    def inv_fact(s):
        n = 1
        while s > math.factorial(n):
            n += 1
        return n

    fns = [
        ['log n',  lambda s: 2 ** s],         # log
        ['sqrt n', lambda s: s ** 2],         # sqrt
        ['n',      lambda s: s],              # linear
        ['n^2',    lambda s: math.sqrt(s)],   # quadratic
        ['2^n',    lambda s: math.log(s, 2)], # exponential
        ['n!',     inv_fact],                 # factorial
    ]

    table = [[''] + periods]
    for label, fn in fns:
        row = [label]
        for t in periods:
            try:
                row.append(fn(t * rate))
            except OverflowError:
                row.append('  <overflow>')
        table.append(row)
    return table

def print_table_text(table):
    for row in table:
        for col in row:
            if type(col) == int:
                print '{0:12d}'.format(col),
            elif type(col) == float:
                print '{0:12g}'.format(col),
            else:
                print '{0:12s}'.format(col),
        print
    print

def print_table_html(table):
    print '
'
    print '  '

    # assume the first row is a header row
    for col in table[0]:
        print '    {0}	'.format(col)
    print '  	'

    # now we print the data
    for row in table[1:]:
        print '
'
        for col in row:
            print '    {0}	'.format(col)
        print '
'
    print '
'

# Show the growth in the problem size, for each function, as a function of time
# in seconds.
def plot_results():
    t = np.arange(1.0, 100.0) # 100 s
    fns = [
        ['log n',  inv_log],
        ['sqrt n', inv_sqrt],
        ['n',      inv_linear],
        ['n^2',    inv_quad],
        ['2^n',    inv_exp],
        ['n!',     inv_fact],
    ]
    for label, fn in fns:
        plt.plot(t, [fn(x) for x in t], label=label)
    plt.yscale('log')
    plt.legend(loc='upper left')
    plt.show()

if __name__ == '__main__':
    print_table_html(make_table())


from __future__ import annotations

import math

import numpy as np
from numpy.linalg import inv, det, slogdet

class UnivariateGaussian:
    """
    Class for univariate Gaussian Distribution Estimator
    """

    def __init__(self, biased_var: bool = False) -> UnivariateGaussian:
        """
        Estimator for univariate Gaussian mean and variance parameters

        Parameters
        ----------
        biased_var : bool, default=False
            Should fitted estimator of variance be a biased or unbiased estimator

        Attributes
        ----------
        fitted_ : bool
            Initialized as false indicating current estimator instance has not been fitted.
            To be set as True in `UnivariateGaussian.fit` function.

        mu_: float
            Estimated expectation initialized as None. To be set in `UnivariateGaussian.fit`
            function.

        var_: float
            Estimated variance initialized as None. To be set in `UnivariateGaussian.fit`
            function.
        """
        self.biased_ = biased_var
        self.fitted_, self.mu_, self.var_ = False, None, None

    def fit(self, X: np.ndarray) -> UnivariateGaussian:
        """
        Estimate Gaussian expectation and variance from given samples

        Parameters
        ----------
        X: ndarray of shape (n_samples, )
            Training data

        Returns
        -------
        self : returns an instance of self.

        Notes
        -----
        Sets `self.mu_`, `self.var_` attributes according to calculated estimation (where
        estimator is either biased or unbiased). Then sets `self.fitted_` attribute to `True`
        """
        if self.biased_:
            m = X.shape[0]
        else:
            m = X.shape[0] - 1
        self.mu_ = np.mean(X)
        self.var_ = np.sum(np.power(X - self.mu_, 2)) / m
        self.fitted_ = True
        return self

    def pdf(self, X: np.ndarray) -> np.ndarray:
        """
        Calculate PDF of observations under Gaussian model with fitted estimators

        Parameters
        ----------
        X: ndarray of shape (n_samples, )
            Samples to calculate PDF for

        Returns
        -------
        pdfs: ndarray of shape (n_samples, )
            Calculated values of given samples for PDF function of N(mu_, var_)

        Raises
        ------
        ValueError: In case function was called prior fitting the model
        """
        if not self.fitted_:
            raise ValueError("Estimator must first be fitted before calling `pdf` function")
        args = [self.mu_, self.var_]
        B = np.apply_along_axis(self.normal_dist_density, 0, [X], *args).reshape(X.shape[0], )
        return B

    @staticmethod
    def normal_dist_density(x: float, mu: float, sigma: float) -> float:
        power = -1 * (x - mu) ** 2 / 2 * sigma
        return 1 / (2 * math.pi * sigma) ** 0.5 * math.exp(power)

    @staticmethod
    def log_likelihood(mu: float, sigma: float, X: np.ndarray) -> float:
        """
        Calculate the log-likelihood of the data under a specified Gaussian model

        Parameters
        ----------
        mu : float
            Expectation of Gaussian
        sigma : float
            Variance of Gaussian
        X : ndarray of shape (n_samples, )
            Samples to calculate log-likelihood with

        Returns
        -------
        log_likelihood: float
            log-likelihood calculated
        """
        # args = (mu, sigma)
        # A = np.apply_along_axis(UnivariateGaussian.normal_dist_density, 0, [X], *args).reshape(X.shape[0], )
        # return math.log(np.product(A))
        sum = np.sum((X - mu) ** 2)
        m = X.shape[0]
        return math.log(1 / ((2 * math.pi * math.sqrt(sigma)) ** (m / 2))) - 1 / (2 * sigma) * sum

class MultivariateGaussian:
    """
    Class for multivariate Gaussian Distribution Estimator
    """

    def __init__(self):
        """
        Initialize an instance of multivariate Gaussian estimator

        Attributes
        ----------
        fitted_ : bool
            Initialized as false indicating current estimator instance has not been fitted.
            To be set as True in `MultivariateGaussian.fit` function.

        mu_: ndarray of shape (n_features,)
            Estimated expectation initialized as None. To be set in `MultivariateGaussian.fit`
            function.

        cov_: ndarray of shape (n_features, n_features)
            Estimated covariance initialized as None. To be set in `MultivariateGaussian.fit`
            function.
        """
        self.mu_, self.cov_ = None, None
        self.fitted_ = False

    def fit(self, X: np.ndarray) -> MultivariateGaussian:
        """
        Estimate Gaussian expectation and covariance from given samples

        Parameters
        ----------
        X: ndarray of shape (n_samples, n_features)
            Training data

        Returns
        -------
        self : returns an instance of self

        Notes
        -----
        Sets `self.mu_`, `self.cov_` attributes according to calculated estimation.
        Then sets `self.fitted_` attribute to `True`
        # """
        self.mu_ = np.mean(X, 0)
        # # self.cov_ = np.matmul(np.transpose(X - self.mu_), X - self.mu_) * 1 / (X.shape[0] - 1)
        self.cov_ = np.cov(np.transpose(X))
        self.fitted_ = True
        return self

    @staticmethod
    def mult_gaus_density(X: np.ndarray, sigma: np.ndarray, meu: np.ndarray):
        d = meu.shape[0]
        power = np.linalg.multi_dot([np.transpose(X - meu), np.linalg.inv(sigma), X - meu]) * (-0.5)
        return 1 / ((2 * math.pi) ** d * np.linalg.det(sigma)[1]) * math.exp(power)

    def pdf(self, X: np.ndarray):
        """
        Calculate PDF of observations under Gaussian model with fitted estimators

        Parameters
        ----------
        X: ndarray of shape (n_samples, n_features)
            Samples to calculate PDF for

        Returns
        -------
        pdfs: ndarray of shape (n_samples, )
            Calculated values of given samples for PDF function of N(mu_, cov_)

        Raises
        ------
        ValueError: In case function was called prior fitting the model
        """
        if not self.fitted_:
            raise ValueError("Estimator must first be fitted before calling `pdf` function")
        args = (self.cov_, self.mu_)
        B = np.apply_along_axis(MultivariateGaussian.mult_gaus_density, 1, X, *args)
        return B

    @staticmethod
    def log_likelihood(mu: np.ndarray, cov: np.ndarray, X: np.ndarray) -> float:
        """
        Calculate the log-likelihood of the data under a specified Gaussian model

        Parameters
        ----------
        mu : ndarray of shape (n_features,)
            Expectation of Gaussian
        cov : ndarray of shape (n_features, n_features)
            covariance matrix of Gaussian
        X : ndarray of shape (n_samples, n_features)
            Samples to calculate log-likelihood with

        Returns
        -------
        log_likelihood: float
            log-likelihood calculated over all input data and under given parameters of Gaussian
        """
        d = X.shape[1]
        n = X.shape[0]
        sum = np.sum(np.multiply(np.transpose(X - mu), np.matmul(np.linalg.inv(cov), np.transpose(X - mu))))
        return n / 2 * (math.log(1) - math.log((math.pi * 2) ** d) + np.linalg.slogdet(cov)[1]) - 0.5 * sum
     #closed expression to calculate log likelihood
# from __future__ import annotations
# import numpy as np
#
#
# class UnivariateGaussian:
#     """
#     Class for univariate Gaussian Distribution Estimator
#     """
#     def __init__(self, biased_var: bool = False) -> UnivariateGaussian:
#         """
#         Estimator for univariate Gaussian mean and variance parameters
#
#         Parameters
#         ----------
#         biased_var : bool, default=False
#             Should fitted estimator of variance be a biased or unbiased estimator
#
#         Attributes
#         ----------
#         fitted_ : bool
#             Initialized as false indicating current estimator instance has not been fitted.
#             To be set as True in `UnivariateGaussian.fit` function.
#
#         mu_: float
#             Estimated expectation initialized as None. To be set in `UnivariateGaussian.fit`
#             function.
#
#         var_: float
#             Estimated variance initialized as None. To be set in `UnivariateGaussian.fit`
#             function.
#         """
#         self.biased_ = biased_var
#         self.fitted_, self.mu_, self.var_ = False, None, None
#
#     def fit(self, X: np.ndarray) -> UnivariateGaussian:
#         """
#         Estimate Gaussian expectation and variance from given samples
#
#         Parameters
#         ----------
#         X: ndarray of shape (n_samples, )
#             Training data
#
#         Returns
#         -------
#         self : returns an instance of self.
#
#         Notes
#         -----
#         Sets `self.mu_`, `self.var_` attributes according to calculated estimation (where
#         estimator is either biased or unbiased). Then sets `self.fitted_` attribute to `True`
#         """
#         self.mu_ = UnivariateGaussian._calc_mu(X)
#         if self.biased_:
#             self.var_ = UnivariateGaussian._calc_unbiased_var(self.mu_, X)
#         else:
#             self.var_ = UnivariateGaussian._calc_biased_var(self.mu_, X)
#
#         self.fitted_ = True
#         return self
#
#     def pdf(self, X: np.ndarray) -> np.ndarray:
#         """
#         Calculate PDF of observations under Gaussian model with fitted estimators
#         Parameters
#         ----------
#         X: ndarray of shape (n_samples, )
#             Samples to calculate PDF for
#         Returns
#         -------
#         pdfs: ndarray of shape (n_samples, )
#             Calculated values of given samples for PDF function of N(mu_, var_)
#         Raises
#         ------
#         ValueError: In case function was called prior fitting the model
#         """
#         if not self.fitted_:
#             raise ValueError("Estimator must first be fitted before calling `pdf` function")
#         return UnivariateGaussian._calc_pdf(self.mu_, self.var_, X)
#
#     @staticmethod
#     def _calc_pdf(mu: float, var: float, X: np.ndarray) -> np.ndarray:
#         """
#         Calculate PDF of observations under Gaussian model if random variable X~N(mu, var)
#         Parameters
#         ----------
#         mu : float
#             Expectation of Gaussian
#         var : float
#             Variance of Gaussian
#         X: ndarray of shape (n_samples, )
#             Samples to calculate PDF for
#         Returns
#         -------
#         pdfs: ndarray of shape (n_samples, )
#             Calculated values of given samples for PDF function of N(mu_, var_)
#         """
#         scalar1 = 1 / np.sqrt(2 * np.pi * var)
#         scalar2 = -2 * var
#         return scalar1 * np.exp((np.power((X - mu), 2)) / scalar2)
#
#     @staticmethod
#     def _calc_mu(X: np.ndarray) -> float:
#         """
#         Calculate mean estimator of observations under Gaussian model
#
#         Parameters
#         ----------
#         X: ndarray of shape (n_samples, )
#             Samples to calculate mean for
#
#         Returns
#         -------
#         mean: float
#            mean calculated
#         """
#         if len(X) == 0:  # Don't divide by 0, if the array is empty return mean = 0
#             return 0
#         return X.sum() / len(X)
#
#     @staticmethod
#     def _calc_biased_var(mu: float, X: np.ndarray) -> float:
#         """
#         Calculate variance estimator of observations under Gaussian model for biased estimator
#
#         Parameters
#         ----------
#         mu : float
#             Expectation of Gaussian
#         X: ndarray of shape (n_samples, )
#             Samples to calculate mean for
#
#         Returns
#         -------
#         variance: float
#            variance calculated
#         """
#         if len(X) == 0:  # Don't divide by 0, if the array is empty return var = 0
#             return 0
#         scalar = 1 / len(X)
#         return scalar * np.sum(np.power((X - mu), 2))
#
#     @staticmethod
#     def _calc_unbiased_var(mu: float, X: np.ndarray) -> float:
#         """
#         Calculate variance estimator of observations under Gaussian model for unbiased estimator
#
#         Parameters
#         ----------
#         mu : float
#             Expectation of Gaussian
#         X: ndarray of shape (n_samples, )
#             Samples to calculate mean for
#
#         Returns
#         -------
#         variance: float
#            variance calculated
#         """
#         if len(X) <= 1:  # Don't divide by 0, if the array is empty or single sample return var = 0
#             return 0
#         scalar = 1 / (len(X) - 1)
#         return scalar * np.sum(np.power((X - mu), 2))
#
#     @staticmethod
#     def log_likelihood(mu: float, sigma: float, X: np.ndarray) -> float:
#         """
#         Calculate the log-likelihood of the data under a specified Gaussian model
#
#         Parameters
#         ----------
#         mu : float
#             Expectation of Gaussian
#         sigma : float
#             Variance of Gaussian
#         X : ndarray of shape (n_samples, )
#             Samples to calculate log-likelihood with
#
#         Returns
#         -------
#         log_likelihood: float
#             log-likelihood calculated
#         """
#         return np.sum(np.log(UnivariateGaussian._calc_pdf(mu, sigma, X)))
#
#
# class MultivariateGaussian:
#     """
#     Class for multivariate Gaussian Distribution Estimator
#     """
#     def __init__(self):
#         """
#         Initialize an instance of multivariate Gaussian estimator
#
#         Attributes
#         ----------
#         fitted_ : bool
#             Initialized as false indicating current estimator instance has not been fitted.
#             To be set as True in `MultivariateGaussian.fit` function.
#
#         mu_: ndarray of shape (n_features,)
#             Estimated expectation initialized as None. To be set in `MultivariateGaussian.fit`
#             function.
#
#         cov_: ndarray of shape (n_features, n_features)
#             Estimated covariance initialized as None. To be set in `MultivariateGaussian.fit`
#             function.
#         """
#         self.mu_, self.cov_ = None, None
#         self.fitted_ = False
#
#     def fit(self, X: np.ndarray) -> MultivariateGaussian:
#         """
#         Estimate Gaussian expectation and covariance from given samples
#
#         Parameters
#         ----------
#         X: ndarray of shape (n_samples, n_features)
#             Training data
#
#         Returns
#         -------
#         self : returns an instance of self
#
#         Notes
#         -----
#         Sets `self.mu_`, `self.cov_` attributes according to calculated estimation.
#         Then sets `self.fitted_` attribute to `True`
#         """
#
#         self.mu_ = MultivariateGaussian._calc_mu(X)
#         self.cov_ = MultivariateGaussian._calc_unbiased_cov(self.mu_, X)
#         self.fitted_ = True
#         return self
#
#     def pdf(self, X: np.ndarray):
#         """
#         Calculate PDF of observations under Gaussian model with fitted estimators
#
#         Parameters
#         ----------
#         X: ndarray of shape (n_samples, n_features)
#             Samples to calculate PDF for
#
#         Returns
#         -------
#         pdfs: ndarray of shape (n_samples, )
#             Calculated values of given samples for PDF function of N(mu_, cov_)
#
#         Raises
#         ------
#         ValueError: In case function was called prior fitting the model
#         """
#         if not self.fitted_:
#             raise ValueError("Estimator must first be fitted before calling `pdf` function")
#         # from scipy.stats import multivariate_normal
#         # var = multivariate_normal(self.mu_, self.cov_)
#         # real_pdf = var.pdf(X)
#
#         # m = X - self.mu_
#         # scalar = 1 / (np.sqrt(np.power(2 * np.pi, len(self.mu_)) * np.linalg.det(self.cov_)))
#         # pdf = np.zeros(len(X))
#         # for i in range(len(pdf)):
#         #     pdf[i] = scalar * np.exp((-1 / 2) * (m[i].T @ np.linalg.inv(self.cov_) @ m[i]))
#         return MultivariateGaussian._calc_pdf(self.mu_, self.cov_, X)
#
#     @staticmethod
#     def log_likelihood(mu: np.ndarray, cov: np.ndarray, X: np.ndarray) -> float:
#         """
#         Calculate the log-likelihood of the data under a specified Gaussian model
#
#         Parameters
#         ----------
#         mu : ndarray of shape (n_features,)
#             Expectation of Gaussian
#         cov : ndarray of shape (n_features, n_features)
#             covariance matrix of Gaussian
#         X : ndarray of shape (n_samples, n_features)
#             Samples to calculate log-likelihood with
#
#         Returns
#         -------
#         log_likelihood: float
#             log-likelihood calculated over all input data and under given parameters of Gaussian
#         """
#
#         return np.float(np.sum(np.log(MultivariateGaussian._calc_pdf(mu, cov, X))))
        # loglikelihood = -0.5 * (
        #         np.log(np.linalg.det(cov))
        #         + np.einsum('...j,jk,...k', X, np.linalg.inv(cov), X)
        #         + len(mu) * np.log(2 * np.pi)
        # )
        # loglikelihood = np.sum(loglikelihood)

        # if a == loglikelihood:
        #     print(a)

        # from scipy.stats import multivariate_normal
        # y = multivariate_normal.pdf(X, mean=mu, cov=cov)
        # y_log = np.log(y)
        # from scipy.stats import norm
        # t, s = norm.fit(X)
        # log_likelihood_2 = np.sum(np.log(norm.pdf(X, t, s)))

        # def calc_loglikelihood(residuals):
        #     return -0.5 * (np.log(np.linalg.det(cov)) + residuals.T.dot(np.linalg.inv(cov)).dot(residuals) + 2 * np.log(
        #         2 * np.pi))
        #
        # m = X - mu
        # b = np.apply_along_axis(calc_loglikelihood, 1, m)
        # c = b.sum()
        # if a == c:
        #     print(a)

        # vals, vecs = np.linalg.eigh(cov)
        # logdet = np.sum(np.log(vals))
        # valsinv = np.array([1. / v for v in vals])
        # # `vecs` is R times D while `vals` is a R-vector where R is the matrix
        # # rank. The asterisk performs element-wise multiplication.
        # U = vecs * np.sqrt(valsinv)
        # rank = len(vals)
        # dev = X - mu
        # # "maha" for "Mahalanobis distance".
        # maha = np.square(np.dot(dev, U)).sum()
        # log2pi = np.log(2 * np.pi)
        # log_l = -0.5 * (rank * log2pi + maha + logdet)
        # if a == log_l:
        #     print(a)
        # return c

    # @staticmethod
    # def _calc_mu(X: np.ndarray) -> np.ndarray:
    #     """
    #     Calculate multivariate sample mean estimator under Gaussian model
    #
    #     Parameters
    #     ----------
    #     X: ndarray of shape (n_samples, n_features)
    #         Training data
    #
    #     Returns
    #     -------
    #     mean: ndarray of shape (n_features,)
    #        mean calculated
    #     """
    #     if len(X) == 0:  # Don't divide by 0, if the array is empty return mean = 0
    #         raise ValueError("Non valid samples were given to fit function, samples size must be > 1")
    #     return np.mean(X, axis=0)
    #
    # @staticmethod
    # def _calc_unbiased_cov(mu: np.ndarray, X: np.ndarray) -> np.ndarray:
    #     """
    #     Calculate covariance multivariate estimator of observations under Gaussian model for unbiased estimator
    #
    #     Parameters
    #     ----------
    #     mu : ndarray of shape (n_features,)
    #         Expectation of Gaussian
    #     X: ndarray of shape (n_samples, n_features)
    #         Training data
    #
    #     Returns
    #     -------
    #     covariance: ndarray of shape (n_features,)
    #        covariance calculated
    #     """
    #     if len(X) <= 1:  # Don't divide by 0, if the array is empty or single sample return var = 0
    #         raise ValueError("Non valid samples were given to fit function, samples size must be > 1")
    #     scalar = 1 / (len(X) - 1)
    #     m = X - mu
    #     try:
    #         cov = np.cov(X.T)  # Reminder: come back here and decide which implementation to choose
    #         # cov = scalar * m.T @ m
    #     except:
    #         try:
    #             cov = scalar * m @ m.T
    #         except:
    #             raise ValueError("Non valid samples were given to pdf")
    #     return cov
    #
    # @staticmethod
    # def _calc_pdf(mu: np.ndarray, cov: np.ndarray, X: np.ndarray) -> np.ndarray:
    #     """
    #     Calculate PDF of observations under Gaussian model if random variable X~N(mu, var)
    #     Parameters
    #     ----------
    #     mu : ndarray of shape (n_features,)
    #         Expectation of Gaussian
    #     cov : ndarray of shape (n_features, n_features)
    #         covariance matrix of Gaussian
    #     X: ndarray of shape (n_samples, n_features)
    #         Training data
    #     Returns
    #     -------
    #     pdfs: ndarray of shape (n_samples, )
    #         Calculated values of given samples for PDF function of N(mu_, var_)
    #     """
    #     m = X - mu
    #     scalar = 1 / (np.sqrt(np.power(2 * np.pi, len(mu)) * np.linalg.det(cov)))
    #     pdf = np.zeros(len(X))
    #     for i in range(len(pdf)):
    #         pdf[i] = scalar * np.exp((-0.5) * (m[i].T @ np.linalg.inv(cov) @ m[i]))
    #     return pdf
    #
    #


"""
Problem 9:

A Pythagorean triplet is a set of three natural numbers, a < b < c, for which,

a^2 + b^2 = c^2

For example, 3^2 + 4^2 = 9 + 16 = 25 = 5^2.

There exists exactly one Pythagorean triplet for which a + b + c = 1000.
Find the product abc.
"""

import numpy as np

# Brute force: start computing triplets until the correct one has been found
def find_triplet():
    for a in range(1, 500):
        for b in range(1, 500):

            c = np.sqrt(a**2 + b**2)

            if a + b + c == 1000:
                print(f"Triplet found!")
                print(f"a: {a}, b: {b}, c: {c}")
                print(f"a + b + c = {a + b + c}")
                print(f"Product abc = {a*b*c}")
                return

find_triplet()


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

η = 0.001  # learning rate

train_df = pd.read_csv("train_data.csv")
x_train, y_train = train_df['x_train'], train_df['y_train']

# wired initial weight:
# β0, β1=[
#     (-2.8878, -0.7567),
#     (-1.4769, -0.7439),
#     (-0.265, -1.3396)
# ]

def MSE(β0, β1, x_data, y_data):
    # y = β0 + β1 * x
    # error = 0
    # for x, y in zip(x_data, y_data):
    #     error += (β0 + β1*x - y) ** 2
    return sum((β0 + β1*x_data - y_data) ** 2) / x_data.size

def MAE(β0, β1, x_data, y_data):
    # y = β0 + β1 * x
    # error = 0
    # for x, y in zip(x_data, y_data):
    #     error += abs(β0 + β1*x - y)
    return sum(abs(β0 + β1*x_data - y_data)) / x_data.size

def f(β0, β1, X):
    return β0 + β1*X

def plot_line(β0, β1, X, Y):
    x_min = X.min() - 1
    x_max = X.max() + 1
    # x_dist = int(x_max - x_min) + 1
    # dots = np.linspace(int(x_min)-1, int(x_max)+1, x_dist*3)

    plt.plot(X, Y, '.')
    plt.plot([x_min, x_max], f(β0, β1, np.array([x_min, x_max])))

def MSE_train(x_train, y_train):
    # IN:   [x_train], [y_train]
    # OUT:  [iter_loss], β0, β1
    n = x_train.shape[0]
    β0, β1 = np.random.normal(0, 1, 2)  # μ, σ, shape
    loss = MSE(β0, β1, x_train, y_train)
    loss_record = [loss]
    pre_loss = loss + 1
    # 1st derivation
    # β0 2/n ∑(β0+β1*X_i-Y_i)
    # β1 2/n ∑(β0+β1*X_i-Y_i)*β1
    while(abs(loss - pre_loss) > 0.000001):
        # print("η:", η)
        sum_β0_β1_X_Y = sum(β0 + β1 * x_train - y_train)
        β0 -= η * 2 * sum_β0_β1_X_Y / n
        β1 -= η * 2 * sum_β0_β1_X_Y * β1 / n
        pre_loss = loss
        loss = MSE(β0, β1, x_train, y_train)
        loss_record += [loss]
        print(loss, β0, β1)
    return loss_record, β0, β1

def MAE_train(x_train, y_train):
    # IN:   [x_train], [y_train]
    # OUT:  [iter_loss], β0, β1
    n = x_train.shape[0]
    β0, β1 = np.random.normal(0, 1, 2)  # μ, σ, shape
    loss = MAE(β0, β1, x_train, y_train)
    loss_record = [loss]
    pre_loss = loss + 1
    # 1st derivation
    # β0 2/n ∑(+1 if β0+β1*X_i-Y_i > 0
    #           0 if β0+β1*X_i-Y_i ==0
    #          -1 if β0+β1*X_i-Y_i < 0)
    # β1 2/n ∑(+1 if β0+β1*X_i-Y_i > 0
    #           0 if β0+β1*X_i-Y_i ==0
    #          -1 if β0+β1*X_i-Y_i < 0) * β1
    while(abs(loss - pre_loss) > 0.000001):
        sum_β0_β1_X_Y = sum(
            1 if e > 0 else -1
            for e in (β0 + β1 * x_train - y_train)
        )  # 假裝 β0 + β1 * x_train - y_train == 0 沒發生
        β0 -= η * 2 * sum_β0_β1_X_Y / n
        β1 -= η * 2 * sum_β0_β1_X_Y * β1 / n

        pre_loss = loss
        loss = MAE(β0, β1, x_train, y_train)
        loss_record += [loss]
        print(loss, β0, β1)
    return loss_record, β0, β1

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

loss, β0, β1 = MSE_train(x_train, y_train)
# print("loss:", loss[-1])
# print("β0, β1: ", β0, β1)
# plot_line(β0, β1, x_train, y_train)
b = plt.figure(2)
plt.title('MSE')
plt.plot(range(len(loss)), loss)

loss, β0, β1 = MAE_train(x_train, y_train)
c = plt.figure(3)
plt.title('MAE')
plt.plot(range(len(loss)), loss)
plt.show()

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

# TEST
test_data = pd.read_csv("test_data.csv")
x_test, y_test = test_data['x_test'], test_data['y_test']

# print("test data error:", MSE(β0, β1, x_test, y_test))
# print("test data error:", MAE(β0, β1, x_test, y_test))


"""
Last night you partied a little too hard. Now there's a black and
white photo of you that's about to go viral! You can't let this ruin
your reputation, so you want to apply the box blur algorithm to the
photo to hide its content.

The pixels in the input image are represented as integers. The algorithm
distorts the input image in the following way: Every pixel x in the output
image has a value equal to the average value of the pixel values from the
3 x 3 square that has its center at x, including x itself. All the pixels
on the border od x are then removed.

Return the blurred image as an integer, with the fractions rounded down.

Example

For

image = [[1, 1, 1],
         [1, 7, 1],
         [1, 1, 1]]
the output should be boxBlur(image) = [[1]].

To get the value of the middle pixel in the input 3x3 square:
(1 + 1 + 1 + 1 + 7 + 1 + 1 + 1 + 1) = 15 / 9 = 1.666 = 1
The border pixels are cropped from the final result.
"""

import numpy as np
import math

def boxBlur(image):

    np_image = np.array(image)
    lx = len(image[0]) - 2
    ly = len(image) - 2

    new_img = np.zeros((ly,lx))

    x = 0
    y = 3
    for i in range(ly):
        for j in range(lx):
            t = np_image[x + i: y + i, x + j: y + j]
            t.tolist()
            flat_list = [k for sublist in t for k in sublist]
            s = math.floor(sum(flat_list)/9)
            new_img[i][j] = int(s)

    return(new_img.tolist())

image = [[1,1,1],
         [1,7,1],
         [1,1,1]]

print(boxBlur(image))  # [[1]]


import numpy as np
import pandas as pd
import time, gc
pi = np.pi
cos = np.cos
sin = np.sin
acos = np.arccos
degrees = np.degrees
radians = np.radians

def angularDistance(row):
    '''
    Computes the angular distance (degrees) between the reference Right-Ascension (RA)
    & Declination (Dec) value, and the corresponding RA - Dec value of the stars in
    the dataframe - <OB_CATALOGUE>

    <Formula> - http://spiff.rit.edu/classes/phys373/lectures/radec/radec.html

    Parameters
    ----------
    row : pd.Dataframe - series
        Input RA/Dec in degrees from the <IMG_DF> dataframe

    Returns
    -------
    y : pd.Dataframe - series
        The corresponding angular distance in degree value.
    '''

    # Units of right-ascension is in (hours) format
    alpha1, alpha2 = radians(15*row['RA_1']), radians(15*row['RA_2'])

    # Units of declination is in (degrees) format
    delta1, delta2 = radians(row['Dec_1']), radians(row['Dec_2'])

    temp = cos(pi/2 - delta1)*cos(pi/2 - delta2) + sin(pi/2 - delta1)*sin(pi/2 - delta2)*cos(alpha1 - alpha2)

    return np.degrees(acos(temp))

def genRefCatalogue(mag_limit, no_iter = -1, gen_csv = True):
    '''
    Generates the reference star catalogue for Geometric Voting Algorithm where each row
    of the table has two unique stars and the corresponding angular distance in degrees,
    for all pairs of stars with a specified upper magnitude limit

    Parameters
    ----------
    mag_limit : floating-point number
        The upper magnitude limit of stars that are required in the reference catalogue

    no_iter : integer, default = -1
        Specifies the number of iterations, thereby allowing it to be reduced
        Default value = -1, allows for the completion of the entire catalogue

    gen_csv : boolean, default = True
          If True generates csv files of the reference catalogues

    Returns
    -------
    OB_CATALOGUE : pd.Dataframe
        The corresponding angular distance in degree value.
    '''

    # Start clock-1
    start1 = time.time()

    # Generate restricted catalogue based on upper magnitude limit
    temp0 = CATALOGUE[CATALOGUE.Mag <= mag_limit]

    # Number of rows in the resticted catalogue
    rows = temp0.shape[0]
    # Resets the index of <temp0>
    temp0.index = list(range(rows))

    # Prints total number of stars in <temp0> and the (n)C(2) - combinations
    print('Number of stars - ', rows)
    print('Number of unique combinations = ', (rows-1)*rows/2)

    # Initialize the number of iterations to take place
    no_iter = (rows-1) if no_iter == -1 else no_iter

    for i in range(no_iter):
        # Throws error if an iteration runs beyond number of available rows in <temp0>
        assert i<(rows-1), 'iterating beyond available number of rows'

        # The final iteration is reduntant, as <temp2> will be zero rows
        '''
        if (rows-1-i)==0:
            continue
        '''

        # Generates <temp1> dataframe which has the (i - th) star of <temp0>
        # repetated (rows-1-i) times
        temp1 = pd.DataFrame(columns = ['Star_ID1','RA_1', 'Dec_1', 'Mag_1'])
        s1, ra, dec, mag = temp0.iloc[i]
        temp1.loc[0] = [s1] + [ra] + [dec] + [mag]
        temp1 = pd.concat([temp1]*(rows-1-i), ignore_index=True)

        # Generates <temp2> dataframe by copying values of <temp0> and dropping the first
        # (i + 1) number of stars
        temp2 = temp0
        temp2 = temp2.drop(list(range(i+1)), axis = 0)
        # Resets the index
        temp2.index = list(range(0, rows-1-i))

        # Concatenates <temp1> & <temp2> side-by-side such that resulting <temp3> has (8) columns altogether
        temp3 = pd.concat([temp1, temp2], axis=1)

        # Initializes <temp4> in the first iteratation
        if i == 0:
            temp4 = temp3

        # Append subsequent <temp4> with <temp3> after first iteration
        else:
            temp4 = pd.concat([temp4, temp3], axis = 0, ignore_index=True)

        # Releases memory back to OS
        if i%40 == 0:
            gc.collect()

    gc.collect()
    # Rename columns
    temp4.columns = ['Star_ID1','RA_1', 'Dec_1', 'Mag_1', 'Star_ID2', 'RA_2', 'Dec_2', 'Mag_2']

    if gen_csv == True:
        #Generates CSV of <temp4>
        temp4.to_csv('Processed_Catalogue1.csv', index = False)

    # Stop clock-1
    end1 = time.time() - start1

    # Print time taken
    print('Process 1 - ', end1)

    # Start clock-2
    start2 = time.time()

    # Initialize <OB_CATALOGUE>
    OB_CATALOGUE = temp4

    # Calculate angular distance between the two stars present in every row
    OB_CATALOGUE['Ang_Distance'] = OB_CATALOGUE.apply(angularDistance, axis = 1)

    if gen_csv == True:
        # Generates CSV of <OB_CATALOGUE>
        OB_CATALOGUE.to_csv('Processed_Catalogue2.csv', index = False)

    # Stop clock-2
    end2 = time.time() - start2

    # Print time taken
    print('Process 2 - ', end2)
    print('Total Process ', end1+ end2)

    return OB_CATALOGUE

CATALOGUE = pd.read_csv("Modified Star Catalogue.csv")
# StarID: The database primary key from a larger "master database" of stars
# Mag: The star's apparent visual magnitude
# RA, Dec: The star's right ascension and declination, for epoch 2000.0 (Unit: RA - hrs; Dec - degrees)

# Sorts <CATALOGUE>
CATALOGUE.sort_values('Mag', inplace=True)

# Run function
REF_DF = genRefCatalogue(mag_limit=4, no_iter=-1, gen_csv=False)

# Sort <REF_DF>
REF_DF.sort_values('Ang_Distance', inplace=True)
# Generates CSV of <REF_DF>
REF_DF.to_csv('Processed_Catalogue3.csv', index = False)


import matplotlib.pyplot as plt
import numpy as np
import plotly
import plotly.tools as tls
import plotly.graph_objs as go
# import random
import jsonReader

# import ipywidgets as widgets
# values is the input that DCK will input from the JSON file

def mean(vals):
    sum = 0
    for i in range(len(vals)):
        sum += vals[i]
    return sum / len(vals)

"""
What follows is a ckassical linear approximation of a good trendline, using the method
of least squares as presentedin the textbook Uncertainities and their Measurements
by Ifan Hugh and Thomas Hase, page 74
"""

def delta(vals):
    return len(vals) * sum(mult_tup(vals, vals)) - (sum(vals)) ** 2

def intercept(valx, valy):
    return (sum(mult_tup(valx, valx)) * sum(valy) - sum(valx) * sum(mult_tup(valx, valy))) / delta(valx)

def mult_tup(x, y):
    m = np.zeros(len(x))
    for i in range(len(x)):
        m[i] = x[i] * y[i]
    return m

def gradient(valx, valy):
    """print(valx)
    print(valy)
    print(summ(valx*valy))
    print(delta)"""
    return (len(valx) * sum(mult_tup(valx, valy)) - sum(valx) * sum(valy)) / delta(valx)

def fin_func(varXName, varYName):
    # make values by calling
    values = jsonReader.getDataCollection(varXName, varYName)

    xuser = (0, 0)  # varXName
    yuser = (0, 0)  # varYName
    x, y = zip(*values)  # splits the tuples in values into two lists, x and y

    trial_array = np.linspace(min(x), max(x))
    super_array = np.linspace(min(x), max(x))
    for i in range(len(trial_array)):
        super_array[i] = trial_array[i] * gradient(x, y) + intercept(x, y)

    fig, ax = plt.subplots()
    ax.plot(trial_array, super_array)
    ax.scatter(x, y)
    ax.scatter(xuser, yuser)

    layout = go.Layout(
        title="Plot of:\n" + str(varXName) + " against \n" + str(varYName),
        xaxis=dict(
            title=str(varXName),
            titlefont=dict(
                family='Oswald, monospace',
                size=18
            ),
        ),
        yaxis=dict(
            title=str(varYName),
            titlefont=dict(
                family='Oswald, monospace',
                size=18
            )
        )

    )
    trace1 = go.Scatter(x=x, y=y, mode="markers", name=str(varXName) + " and " + str(varYName))
    # trace2 = go.Scatter(x=xuser, y=yuser, mode = "markers")
    trace3 = go.Scatter(x=trial_array, y=super_array, name="Linear Correlation Line")
    data = [trace1, trace3]  # trace3]
    plotly_fig = go.Figure(data=data, layout=layout)
    plotly.offline.plot(plotly_fig, show_link=False, filename='basic-scatter-plot.html', )
    with open('basic-scatter-plot.html', 'r') as myfile:
        html_words = myfile.read().replace("\n", '')
    return html_words


from __future__ import division
import numpy as np

def tanh(x):
    return np.tanh(x)

def tanh_deriv(x):
    return 1.0 - np.tanh(x)**2

def logistic(x):
    return 1/(1 + np.exp(-x))

def logistic_derivative(x):
    return logistic(x)*(1-logistic(x))

def _scale_to_binary(e, minV, maxV):
    result = ((e-minV)/(maxV-minV))*(1-0)+0
    return result

def rescale_from_binary(e, minV, maxV):
    result = e*(maxV-minV) + minV
    return result

class NeuralNetwork:
    def __init__(self, layers, activation='tanh'):
        """
        :param layers: A list containing the number of units in each layer.
        Should be at least two values
        :param activation: The activation function to be used. Can be
        "logistic" or "tanh"
        """
        np.random.seed(0)
        if activation == 'logistic':
            self.activation = logistic
            self.activation_deriv = logistic_derivative
        elif activation == 'tanh':
            self.activation = tanh
            self.activation_deriv = tanh_deriv

        self.weights = []
        for i in range(1, len(layers) - 1):
            self.weights.append((2*np.random.random((layers[i - 1] + 1, layers[i]
                                + 1))-1)*2.0)

        self.weights.append((2*np.random.random((layers[i] + 1, layers[i +
                            1]))-1)*2.0)

        #print self.weights

    def fit(self, X, y, learning_rate=2, epochs=50000):
        #Set initial temperature to be 27
        X = np.atleast_2d(X)
        #temp = np.full([X.shape[0], X.shape[1]+1],27)#Initial temperature is 27
        temp = np.ones([X.shape[0], X.shape[1]+1])
        temp[:, 0:-1] = X  # adding the bias unit to the input layer
        X = temp #Create a new X but with an extra bias item
        y = np.array(y)

        for k in range(epochs):
            i = np.random.randint(X.shape[0])
            a = [X[i]]

            for l in range(len(self.weights)):
                a.append(self.activation(np.dot(a[l], self.weights[l])))
            error = y[i] - a[-1]
            deltas = [error * self.activation_deriv(a[-1])]

            for l in range(len(a) - 2, 0, -1): # we need to begin at the second to last layer
                deltas.append(deltas[-1].dot(self.weights[l].T)*self.activation_deriv(a[l]))
            deltas.reverse()
            for i in range(len(self.weights)):
                layer = np.atleast_2d(a[i])
                delta = np.atleast_2d(deltas[i])
                self.weights[i] += learning_rate * layer.T.dot(delta)

    def predict(self, x):
        x = np.array(x)
        #temp = np.full([x.shape[0]+1],27)
        temp = np.ones(x.shape[0]+1)
        temp[0:-1] = x
        a = temp
        #print a
        for l in range(0, len(self.weights)):
            #print a
            a = self.activation(np.dot(a, self.weights[l]))
        return a


import numpy as np

class LinearRegressionScratch:
    def __init__(self):
        return None

    def fit(self, X, y, method, learning_rate=0.01, iterations=500, batch_size=32):
        """inputs :
            X : Ndarray : the data we want to train on
            y : Nadarray : the labeled data for regression
            method : string : ["ols","sgd"]. ols refers to Ordinary Least Square Estimators and sgd to Stochastic Gradient Descent
            learning : float : parameter for the sdg method
            iterations : integer : parameter for the sdg
            batch_size : integer : parameter for the sdg method

        """
        # x devient une matrice avec des 1 en bas avec la taille de y
        y = np.reshape(y, (len(y),1), order='C')
        X = np.concatenate([X, np.ones_like(y)], axis=1)
        # we gather X shape
        rows, cols = X.shape
        if method == 'ols':
            # if more raws than columns, we gather the rank of the matrix
            if rows >= cols == np.linalg.matrix_rank(X):
                # We use the famous formula for the OLS
                self.weights = np.matmul(
                    np.matmul(
                        np.linalg.inv(
                            np.matmul(
                                X.transpose(),
                                X)),
                        X.transpose()),
                    y)
            # if less raws than columns => impossible
            else:
                print('X has not full column rank. method=\'solve\' cannot be used.')

        # METHOD Stochastic gradient descent
        elif method == 'sgd':
            self.weights = np.random.normal(scale=1/cols, size=(cols, 1))
            for i in range(iterations):
                Xy = np.concatenate([X, y], axis=1)
                np.random.shuffle(Xy)
                X, y = Xy[:, :-1], Xy[:, -1:]
                for j in range(int(np.ceil(rows/batch_size))):
                    start, end = batch_size*j, np.min([batch_size*(j+1), rows])
                    Xb, yb = X[start:end], y[start:end]
                    gradient = 2*np.matmul(
                        Xb.transpose(),
                        (np.matmul(Xb,
                                   self.weights)
                         - yb))
                    self.weights -= learning_rate*gradient

        else:
            print('unknown method')

        return self

    def predict(self, X):
        """ function to predict the result with a test set based on what have learned the fit method
            X : NdArray: test set
        """

        # If no weights => no fit
        if not hasattr(self, 'weights'):
            print('Cannot predict. You should call the .fit() method first.')
            return

        X = np.concatenate([X, np.ones((X.shape[0], 1))], axis=1)

        if X.shape[1] != self.weights.shape[0]:
            #print(f'Shapes do not match. {X.shape[1]} != {self.weights.shape[0]}')
            return

        return np.matmul(X, self.weights)

    def rmse(self, X, y):
        # we use the fit method from bellow
        y_hat = self.predict(X).ravel()

        # if not prediction we quit
        if y_hat is None:
            return

        # otherwise we apply the formula
        return np.sqrt((np.mean((y_hat - y)**2)))

    def get_weights(self):
        return self.weights[:-1]


import numpy as np
from scipy.stats import ortho_group
from matplotlib import pyplot as plt

class quad_rand:
    def __init__(self, n, seed=135, max_iters=200):
        """
        intitialize random coefficients with given dimention
        """
        np.random.seed(seed)
        self.n = n
        self.b = np.matrix(2 * np.random.rand(n, 1) - 1)
        U = np.matrix(ortho_group.rvs(dim=n))
        D = np.matrix(np.diagflat(np.random.rand(n)))
        self.A = U.T * D * U
        self.max_iters = max_iters

    def f(self, x):
        """
        calculate f(x) with input x
        """
        return np.asscalar(1 / 2 * x.T * self.A * x + self.b.T * x)

    def df(self, x):

        """
        calculate f'(x) with input x
        """
        return self.A * x + self.b

    def grad_desc_const(self, alpha):
        """
        gradient descent with constant step size alpha
        """
        x = np.matrix(np.zeros((self.n, 1)))
        self.fun_values_const = [self.f(x)]
        for i in range(self.max_iters-1):
            # calculate derivative
            d = self.df(x)
            # update x
            x -= alpha * d
            # get new function value
            self.fun_values_const.append(self.f(x))

    def grad_desc_exact(self):
        """
        gradient descent with exact line minimization
        """
        x = np.matrix(np.zeros((self.n, 1)))
        self.fun_values_exact = [self.f(x)]
        for i in range(self.max_iters-1):
            # calculate derivative
            d = self.df(x)
            # calculate step size
            alpha = np.asscalar(d.T * d / (d.T * self.A * d))
            # update x
            x -= alpha * d
            # get new function value
            self.fun_values_exact.append(self.f(x))

    def grad_desc_armijo(self, alpha, beta=0.5, sigma=0.9):
        """
        gradient descent with Armijo step size rule
        """
        x = np.matrix(np.zeros((self.n, 1)))
        self.fun_values_armijo = [self.f(x)]
        for i in range(self.max_iters-1):
            # calculate derivative
            d = self.df(x)
            # backtracking line search
            cur_alpha = alpha
            cur_value = self.f(x - cur_alpha * d)
            while cur_value <= self.fun_values_exact[-1] + sigma * cur_alpha * d.T * d:
                cur_alpha *= beta
                cur_value = self.f(x - cur_alpha * d)
            # update x
            x -= cur_alpha * d
            # get new function value
            self.fun_values_armijo.append(cur_value)

# create my random function
my_fun = quad_rand(100)
# run graident descent
my_fun.grad_desc_const(alpha=0.3)
my_fun.grad_desc_exact()
my_fun.grad_desc_armijo(alpha=1)
# plot
plt.plot(range(200), my_fun.fun_values_const, label='Constant')
plt.plot(range(200), my_fun.fun_values_exact, label='Exact Min')
plt.plot(range(200), my_fun.fun_values_armijo, label='Armijo')
plt.legend(loc="best")
plt.xlabel("Iterations")
plt.ylabel("Function Value")
plt.show()


import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt

description = """ This one is a bit different, and explains how to use
   odds ratios, and the beta distribution, to understand the difference
   between a case and control sample

               Cancer   Control
   Smokers     83       72
   Nonsmokers  3        14

   Here we have 86 individuals in both case and control samples.  83
   smokers obtained cancer, while 3 nonsmokers obtained cancer.  72
   smokers did not obtain cancer, while 3 nonsmokers did not obtain
   cancer.  Ask the question: is there a significant difference
   between the smoking habits in the two groups?

   pL: population proportion of lung-cancer patients who smoke = 83/86
   pC: population proportion of control who smoke  = 72/86

   The question may be be answered by examining the posterior
   distribution of pL - pC.  Or the odds ratio

      pL/(1-pL)  /  pC/(1-pC)

   The log odds ratio lambda is less skewed, so ask the question
   p(lambda>0 | data)

   Overall likelihood is :

     L(pL, pC) = pL^83(1 - pL)^3 pC^72(1 - pC)^14

   pL has a Beta distribution, specifically Beta(83,3)
   pC has Beta(72,14)

   Draw 1000 pL' from scipy.stats.beta(83, 3)
   Draw 1000 pC' from scipy.stats.beta(72, 14)
   Calculate the number of times the odds ratio is > 0!
   """

betaL   = stats.beta(83, 3)
betaC   = stats.beta(72, 14)

ndraws  = 10000
pLp     = betaL.rvs(size=ndraws)
pCp     = betaC.rvs(size=ndraws)

numer   = pLp / (1 - pLp)
denom   = pCp / (1 - pCp)
logodds = np.log(numer/denom)

print "Expectation value of logodds  : %.4f" % (np.mean(logodds))
print "Standard deviation of logodds : %.4f" % (np.std(logodds))
print "P(logodds > 0 | data)         : %.4f" % (1.0*len(np.where(logodds>0)[0])*1.0/ndraws)


import numpy as np

def lines_closest_pts(a, b, c, d):
    """
    Returns two closest pints on the lines AB and CD,
    which detetermines the distance between the lines.
    a, b, c, d: [3] numpy vectors
    return: n, m: [3] numpy vectors of the closest points
    """
    A = np.array([[(b-a).dot(b-a), -(b-a).dot(d-c)],
                  [(d-c).dot(b-a), -(d-c).dot(d-c)]])
    B = -np.array([(b-a).dot(a-c), (d-c).dot(a-c)])
    x = np.linalg.inv(A).dot(B)
    n = a + x[0] * (b-a)
    m = c + x[1] * (d-c)
    return n, m

def lines_angle(a, b, c, d):
    """
    Returns the angle between the lines AB and CD,
    a, b, c, d: [3] numpy vectors
    return: alpha: [3] numpy vector
    """
    A = (a-b) / np.linalg.norm(a-b)
    B = (c-d) / np.linalg.norm(c-d)
    alpha = np.arccos(np.dot(A, B))
    return alpha

if __name__ == "__main__":
    a = np.array([ 1, 2, -2])
    b = np.array([ 2, 3, -2])
    c = np.array([ 0,-1, 5])
    d = np.array([ 0, 1, 5])
    n, m = lines_closest_pts(a, b, c, d)
    assert np.allclose(n, np.array([ 0.,  1., -2.]))
    assert np.allclose(m, np.array([ 0.,  1.,  5.]))

    a = np.array([ 0, 0, -3])
    b = np.array([ 1, 1, -3])
    c = np.array([ 0, 0, 5])
    d = np.array([ 1, 0, 5])
    alpha = lines_angle(a, b, c, d)
    assert np.allclose(alpha, np.pi / 4)


#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
:Purpose:   This module contains the statistical calculations for the project.

:Platform:  Linux/Windows | Python 3.6+
:Developer: J Berendt
:Email:     development@s3dev.uk

:Comments:  n/a

:Example:

    Calculate a linear regression line on an X/Y dataset.:

        >>> from lib.stats import LinearRegression

        >>> linreg = LinearRegression(x, y)
        >>> linreg.calculate()

        >>> # View the intercept value.
        >>> linreg.intercept
        5.6

        >>> # View the slope value.
        >>> linreg.slope
        0.25678

        >>> # Obtain the regression line array.
        >>> y_ = linreg.regression_line

"""
# pylint: disable=line-too-long

import numpy as np

class LinearRegression:
    """Calculate the linear regression of a dataset.

    Args:
        x (np.array): Array of X-values.
        y (np.array): Array of Y-values.

    :Slope Calculation:
        The calculation for the slope itself is borrowed from the
        :func:`scipy.stats.linregress` function. Whose `source code`_ was
        obtained on GitHub.

    .. _source code: https://github.com/scipy/scipy/blob/v1.8.0/scipy/stats/_stats_mstats_common.py#L16-L203

    """

    def __init__(self, x: np.array, y: np.array):
        """LinearRegression class initialiser."""
        self._x = x
        self._y = y
        self._xbar = 0.0
        self._ybar = 0.0
        self._c = 0.0
        self._m = 0.0
        self._line = np.array(())

    @property
    def slope(self):
        """Accessor to the slope value."""
        return self._m

    @property
    def intercept(self):
        """Accessor to the slope's y-intercept."""
        return self._c

    @property
    def regression_line(self):
        """Accessor to the calculated regression line, as y-values."""
        return self._line

    def calculate(self):
        """Calculate the linear regression for the X/Y data arrays.

        The result of the calculation is accessible via the
        :attr:`regression_line` property.

        """
        self._calc_means()
        self._calc_slope()
        self._calc_intercept()
        self._calc_regression_line()

    def _calc_intercept(self):
        """Calculate the intercept as: ybar - m * xbar."""
        self._c = self._ybar - self._m * self._xbar

    def _calc_means(self) -> float:
        """Calculate the mean of the X and Y arrays."""
        self._xbar = self._x.mean()
        self._ybar = self._y.mean()

    def _calc_regression_line(self):
        """Calculate the regression line as: y = mx + c."""
        self._line = self._m * self._x + self._c

    def _calc_slope(self):
        """Calculate the slope value as: R * ( std(y) / std(x) ).

        Per the ``scipy`` source code comments::

            # Average sums of square differences from the mean
            #   ssxm = mean( (x-mean(x))^2 )
            #   ssxym = mean( (x-mean(x)) * (y-mean(y)) )

            ...

            slope = ssxym / ssxm

        """
        ssxm = np.mean( (self._x - self._xbar)**2 )
        ssxym = np.mean( (self._x - self._xbar) * (self._y - self._ybar) )
        self._m = ssxym / ssxm

    @staticmethod
    def _calc_std(data: np.array, ddof: int=1) -> float:  # pragma nocover
        """Calculate the standard deviation.

        Args:
            data (np.array): Array of values.
            ddof (int): Degrees of freedom. Defaults to 1.

        Returns:
            float: Standard deviation of the given values.

        """
        return np.std(data, ddof=ddof)


from sympy.core import (Function, Pow, sympify, Expr)
from sympy.core.relational import Relational
from sympy.polys import Poly, decompose
from sympy.utilities.misc import func_name

def decompogen(f, symbol):
    """
    Computes General functional decomposition of ``f``.
    Given an expression ``f``, returns a list ``[f_1, f_2, ..., f_n]``,
    where::
              f = f_1 o f_2 o ... f_n = f_1(f_2(... f_n))

    Note: This is a General decomposition function. It also decomposes
    Polynomials. For only Polynomial decomposition see ``decompose`` in polys.

    Examples
    ========

    >>> from sympy.solvers.decompogen import decompogen
    >>> from sympy.abc import x
    >>> from sympy import sqrt, sin, cos
    >>> decompogen(sin(cos(x)), x)
    [sin(x), cos(x)]
    >>> decompogen(sin(x)**2 + sin(x) + 1, x)
    [x**2 + x + 1, sin(x)]
    >>> decompogen(sqrt(6*x**2 - 5), x)
    [sqrt(x), 6*x**2 - 5]
    >>> decompogen(sin(sqrt(cos(x**2 + 1))), x)
    [sin(x), sqrt(x), cos(x), x**2 + 1]
    >>> decompogen(x**4 + 2*x**3 - x - 1, x)
    [x**2 - x - 1, x**2 + x]

    """
    f = sympify(f)
    if not isinstance(f, Expr) or isinstance(f, Relational):
        raise TypeError('expecting Expr but got: `%s`' % func_name(f))
    if symbol not in f.free_symbols:
        return [f]

    result = []

    # ===== Simple Functions ===== #
    if isinstance(f, (Function, Pow)):
        from sympy import S
        if f.is_Pow and f.base == S.Exp1:
            arg = f.exp
        else:
            arg = f.args[0]
        if arg == symbol:
            return [f]
        result += [f.subs(arg, symbol)] + decompogen(arg, symbol)
        return result

    # ===== Convert to Polynomial ===== #
    fp = Poly(f)
    gens = list(filter(lambda x: symbol in x.free_symbols , fp.gens))

    if len(gens) == 1 and gens[0] != symbol:
        f1 = f.subs(gens[0], symbol)
        f2 = gens[0]
        result += [f1] + decompogen(f2, symbol)
        return result

    # ===== Polynomial decompose() ====== #
    try:
        result += decompose(f)
        return result
    except ValueError:
        return [f]

def compogen(g_s, symbol):
    """
    Returns the composition of functions.
    Given a list of functions ``g_s``, returns their composition ``f``,
    where:
        f = g_1 o g_2 o .. o g_n

    Note: This is a General composition function. It also composes Polynomials.
    For only Polynomial composition see ``compose`` in polys.

    Examples
    ========

    >>> from sympy.solvers.decompogen import compogen
    >>> from sympy.abc import x
    >>> from sympy import sqrt, sin, cos
    >>> compogen([sin(x), cos(x)], x)
    sin(cos(x))
    >>> compogen([x**2 + x + 1, sin(x)], x)
    sin(x)**2 + sin(x) + 1
    >>> compogen([sqrt(x), 6*x**2 - 5], x)
    sqrt(6*x**2 - 5)
    >>> compogen([sin(x), sqrt(x), cos(x), x**2 + 1], x)
    sin(sqrt(cos(x**2 + 1)))
    >>> compogen([x**2 - x - 1, x**2 + x], x)
    -x**2 - x + (x**2 + x)**2 - 1
    """
    if len(g_s) == 1:
        return g_s[0]

    foo = g_s[0].subs(symbol, g_s[1])

    if len(g_s) == 2:
        return foo

    return compogen([foo] + g_s[2:], symbol)


import json

from pydantic import BaseModel

import asyncio
import aiohttp
import logging

# import F1.Bot as Bot
import F2.APIParams as APIParams
import F2.Event as Event
import F2.Message as Message
import utils.utils as utils

import sympy
import math

async def isprime(regular_message: Message.Message, event:Event.MessageEvent, *, bot) -> Message.Message:
    """
        判断一个数是否是质数。

        格式：isprime arg1
            arg1: 要判断的数
    """
    n = int(regular_message[1].data['text'])

    return Message.Message.init_with_segments(
        Message.MessageSegment.text("isprime: True" if sympy.isprime(n) else "isprime: False")
    )

async def gcd(regular_message: Message.Message, event:Event.MessageEvent, *, bot) -> Message.Message:
    """
        最大公因数。

        格式：gcd x y
            x, y: 要求的数
    """
    x = int(regular_message[1].data['text'])
    y = int(regular_message[2].data['text'])
    ans = math.gcd(x, y)

    return Message.Message.init_with_segments(
        Message.MessageSegment.text(str(ans))
    )

async def lcm(regular_message: Message.Message, event:Event.MessageEvent, *, bot) -> Message.Message:
    """
        最小公倍数。

        格式：lcm x y
            x, y: 要求的数
    """
    x = int(regular_message[1].data['text'])
    y = int(regular_message[2].data['text'])
    ans = sympy.lcm(x, y)

    return Message.Message.init_with_segments(
        Message.MessageSegment.text(str(ans))
    )

async def log(regular_message: Message.Message, event:Event.MessageEvent, *, bot) -> Message.Message:
    """
        对数。

        格式：log b x  (or) log x
            b: 基
            x: 要求的数
    """
    x = float(regular_message[1].data['text'])
    if len(regular_message) == 3:
        y = float(regular_message[2].data['text'])
        ans = math.log(y, x)
    else:
        ans = math.log(x)

    return Message.Message.init_with_segments(
        Message.MessageSegment.text(str(ans))
    )

async def sqrt(regular_message: Message.Message, event:Event.MessageEvent, *, bot) -> Message.Message:
    """
        开方。

        格式：sqrt x
            x: 要求的数
    """
    x = float(regular_message[1].data['text'])
    ans = math.sqrt(x)

    return Message.Message.init_with_segments(
        Message.MessageSegment.text(str(ans))
    )

async def sin(regular_message: Message.Message, event:Event.MessageEvent, *, bot) -> Message.Message:
    """
        三角函数sin。

        格式：sin x
            x: 要求的数
    """
    x = float(regular_message[1].data['text'])
    ans = math.sin(x)

    return Message.Message.init_with_segments(
        Message.MessageSegment.text(str(ans))
    )

async def cos(regular_message: Message.Message, event:Event.MessageEvent, *, bot) -> Message.Message:
    """
        三角函数cos。

        格式：cos x
            x: 要求的数
    """
    x = float(regular_message[1].data['text'])
    ans = math.cos(x)

    return Message.Message.init_with_segments(
        Message.MessageSegment.text(str(ans))
    )

async def tan(regular_message: Message.Message, event:Event.MessageEvent, *, bot) -> Message.Message:
    """
        三角函数tan。

        格式：tan x
            x: 要求的数
    """
    x = float(regular_message[1].data['text'])
    ans = math.tan(x)

    return Message.Message.init_with_segments(
        Message.MessageSegment.text(str(ans))
    )


import numpy
import torch
from torch import einsum

# 'expression-with-axis-ids -> output-ids'
# Transpose
from torch.nn.functional import softmax

a = torch.arange(6).view(2, 3)
# print(a)
# print(einsum("ij->ji", a))

# Sum Rows
# print(einsum("ij->i", a))

# Sum Columns
# print(einsum("ij->j", a))

# Sum
# print(einsum("ij->", a))

# element-wise multiplication
# print(einsum("ij,ij->ij", a, a))

# Dot product
x = torch.arange(3)
y = torch.arange(3, 6)
# print(einsum("i, i->", x, y))

# Outer Product
# print(einsum("i,j->ij", x, y))

# Matrix Multiplication
x = torch.arange(6).view(2, 3)
y = torch.arange(12).view(3, 4)
# print(einsum("ij,jk->ik", x, y))

# Batch Matrix multiplication
x = torch.arange(6).view(1, 2, 3)
y = torch.arange(12).view(2, 3, 2)
# print(einsum("ijk,ikl->ijl", x, y))

# 4-D matrix multiplication
x = torch.arange(64).view(2, 4, 2, 4)
y = torch.arange(96).view(2, 4, 4, 3)
# print(einsum("bhmd,bhdn->bhmn", x, y))

# Dot Product of 4-D tensors with axis rotation
# q = torch.arange(24).view(2, 2, 2, 3)
# k = torch.arange(24, 48).view(2, 2, 2, 3)
b = 1
h = 2
d = 3
q = torch.tensor([[[0., 1., 1., 0., 1., 1.],
                   [4., 6., 0., 4., 6., 0.],
                   [2., 3., 1., 2., 3., 1.]]]).view(b, -1, h, d)
k = torch.tensor([[[1., 1., 1., 1., 1., 1.],
                   [4., 2., 2., 4., 2., 2.],
                   [3., 1., 2., 3., 1., 2.]]]).view(b, -1, h, d)
v = torch.tensor([[[1., 1., 2., 1., 1., 2.],
                   [2., 4., 4., 2., 4., 4.],
                   [2., 2., 3., 2., 2., 3.]]]).view(b, -1, h, d)
print('Query')
print(q)
print('Key')
print(k)
attn_score = einsum("bmhd,bnhd->bhmn", q, k).type(torch.float32)
print(attn_score)
attn_score_probability = softmax(attn_score, dim=-1)
print(numpy.round(attn_score_probability.detach(), decimals=2))
print(v)
output = einsum("bhmn,bnhd->bmhd", attn_score_probability, v)
print(output)
print(output.reshape(b, -1, h*d))
# In the above example, we have m, and n varying size and sum is over axis d (last axis)