import math
import random
import numpy
import pandas
from scipy.integrate import odeint

"""
This models the ODE of an unregulated genetic expression of a prokaryotic cell
k0 is the rate at which mRNA is being generated
k1 is the rate at which proteins are being
dm and dp are the degradation constants of mRNA and proteins respectively
C1 and C2 are integration constants
m0 and p0 are the mRNA and protein concentrations in M
"""

class UnregulatedGeneExpression:

    # t is a number
    # const is an array of numbers that represent the constants of the reactions
    def __init__(self, tmax=10, num_of_datapoints=1000, m0=0, p0=0, const=(1, 1, .1, .1)):
        self.n = num_of_datapoints
        self.tmax = tmax
        self.m0 = m0
        self.p0 = p0
        self.k0 = const[0]
        self.k1 = const[1]
        self.dm = const[2]
        self.dp = const[3]
        self.t = numpy.linspace(0, self.tmax, self.n)  # time points

    def analytical_solution(self, t):
        C1 = self.m0 - self.k0 / self.dm
        C2 = self.p0 - (self.k1 * self.k0) / (self.dm * self.dp) + C1 * self.k1 / (self.dm - self.dp)
        m_rna = C1 * math.exp(-self.dm * t) + self.k0 / self.dm
        protein = (self.k1 * self.k0) / (self.dm * self.dp) - (C1 * self.k1 * math.exp(-self.dm * t)) / (self.dm - self.dp) + C2 * math.exp(-self.dp * t)
        return m_rna, protein

    def analytical_sim(self):
        # store solutions
        m = numpy.empty_like(self.t)
        p = numpy.empty_like(self.t)

        # record initial conditions
        m[0] = self.m0
        p[0] = self.p0
        # iterate over time:
        for i in range(1, self.n):
            z = self.analytical_solution(i)

            # store solution for plotting
            m[i] = z[0]
            p[i] = z[1]

        dfp = pandas.DataFrame()
        dfp["Time"] = self.t
        dfp["Proteins"] = p

        dfm = pandas.DataFrame()
        dfm["Time"] = self.t
        dfm["mRNA"] = m
        return dfm, dfp

    # Use scipy.odeint to evaluate
    def numerical_solution(self, z, t):
        m0 = z[0]
        p0 = z[1]
        dmdt = self.k0 - self.dm * m0
        dpdt = self.k1 * m0 - self.dp * p0
        dzdt = dmdt, dpdt
        return dzdt

    def numerical_sim(self):
        # store solutions
        m = numpy.empty_like(self.t)
        p = numpy.empty_like(self.t)
        # record initial conditions
        z0 = [self.m0, self.p0]
        m[0] = self.m0
        p[0] = self.p0
        # solve ODE
        for i in range(1, self.n):
            # span for next time step
            tspan = [self.t[i - 1], self.t[i]]

            # solve for next step
            z = odeint(self.numerical_solution, z0, tspan)

            # store solution for plotting
            m[i] = z[1][0]
            p[i] = z[1][1]
            # next initial condition
            z0 = z[1]

        dfp = pandas.DataFrame()
        dfp["Time"] = self.t
        dfp["Proteins"] = p

        dfm = pandas.DataFrame()
        dfm["Time"] = self.t
        dfm["mRNA"] = m
        return dfm, dfp

class GillespieUnregulatedGeneExpression:

    # const is an array of numbers that represent the constants of the reactions
    def __init__(self, tmax=10, m0=0, p0=0, const=None, num_cells=1):
        if const is None:
            const = [1, 1, .1, .1]
        self.tmax = tmax
        self.k0 = const[0]
        self.k1 = const[1]
        self.dm = const[2]
        self.dp = const[3]
        self.Nm = m0
        self.Np = p0
        self.num_cells = num_cells

    def initial_state(self):
        return [self.Nm, self.Np]

    def update_propensities(self, Nm, Np):
        a1 = self.k0
        a2 = self.k1 * Nm
        a3 = self.dm * Nm
        a4 = self.dp * Np

        total = a1 + a2 + a3 + a4

        r1 = a1/total
        r2 = a2/total
        r3 = a3/total
        r4 = a4/total

        return [r1, r2, r3, r4, total]

    def next_reaction(self, r):
        n = random.uniform(0, 1)
        # Create an mRNA
        if 0 <= n <= r[0]:
            return [1, 0]
        # Create a protein
        elif r[0] < n <= r[1] + r[0]:
            return [0, 1]
        # Delete an mRNA
        elif r[1] + r[0] < n <= r[2] + r[1] + r[0]:
            return [-1, 0]
        # Delete a protein
        elif r[2] + r[1] + r[0] < n <= r[3] + r[2] + r[1] + r[0]:
            return [0, -1]
        # no reaction occurs
        elif r[3] + r[2] + r[1] + r[0] < n < 1:
            return [0, 0]

    def time_to_next_rxn(self, a, t):
        dt = -math.log(1 - random.uniform(0, 1))/a[4]
        t = t + dt
        return t

    def update_reaction_vector(self, current_state, update_vector):
        current_state = numpy.array(current_state)
        update_vector = numpy.array(update_vector)
        updated = current_state + update_vector
        return updated

    def run_sim(self):
        time = []
        protein = []
        mrna = []
        t = 0  # start time
        r0 = self.initial_state()
        mrna.append(r0[0])
        protein.append(r0[1])
        time.append(t)
        while t <= self.tmax:
            a = self.update_propensities(r0[0], r0[1])
            next_rxn = self.next_reaction(a)
            t = self.time_to_next_rxn(a, t)
            r = self.update_reaction_vector(r0, next_rxn)
            mrna.append(r[0])
            protein.append(r[1])
            time.append(t)
            r0 = r

        dfp = pandas.DataFrame()
        dfp["Time"] = time
        dfp["Proteins"] = protein

        dfm = pandas.DataFrame()
        dfm["Time"] = time
        dfm["mRNA"] = mrna
        return dfm, dfp

    def multiple_cells_sim(self):
        dfp_multiple = pandas.DataFrame()
        dfm_multiple = pandas.DataFrame()
        dfmt_multiple = pandas.DataFrame()
        dfpt_multiple = pandas.DataFrame()
        if self.num_cells == 1:
            return self.run_sim()
        elif self.num_cells > 1:
            for i in range(0, self.num_cells):
                mrna, prot = self.run_sim()
                dfm_multiple["Run{n}".format(n=i)] = mrna["mRNA"]
                dfp_multiple["Run{n}".format(n=i)] = prot["Proteins"]
                dfmt_multiple["mRNA_Run_time{t}".format(t=i)] = mrna["Time"]
                dfpt_multiple["prot_Run_time{t}".format(t=i)] = prot["Time"]

                dfm_multiple = dfm_multiple.dropna()
                dfp_multiple = dfp_multiple.dropna()
                dfmt_multiple = dfmt_multiple.dropna()
                dfpt_multiple = dfpt_multiple.dropna()

            # Add average col to dataframes
            dfm_multiple["Average"], dfmt_multiple["Average_Time"] = dfm_multiple.mean(axis=1), \
                                                                     dfmt_multiple.mean(axis=1)
            dfp_multiple["Average"], dfpt_multiple["Average_Time"] = dfp_multiple.mean(axis=1), \
                                                                     dfpt_multiple.mean(axis=1)

            # Join the DataFrams
            dfm_final = pandas.concat([dfm_multiple, dfmt_multiple], axis=1, sort=False)
            dfp_final = pandas.concat([dfp_multiple, dfpt_multiple], axis=1, sort=False)
            return dfm_final, dfp_final


# Karhunen-Loève Transform
# Same as pca, this code is a demo on dataset 'ColorHistogram.asc'
import numpy as np

data = np.loadtxt('ColorHistogram.asc', dtype=np.float64)
data = data[:,1:]

means = np.mean(data, axis = 0)
data -= means

data = data.T
print(means.shape)

print(data.shape)

C = (data @ data.T) / data.shape[1]

eig, P = np.linalg.eig(C)
print("eig = ", eig)
print("P = ", P)

P1 = P.T[:16, :]
print("select first 16 eig: ", eig[:16])
#print(P.T @ C @ P)
#print(P1 @ C @ P1.T)

# compress and rebuild
data1 = P1.T @ (P1 @ data)

print("original data[0] vector: ", data[0])
print("rebuild  data[0] vector: ", data1[0])


import numpy as np
from scipy.stats import chi2
from statsmodels.stats.diagnostic import acorr_ljungbox

# Helper Functions
def auto_covariance(series, h):
    n = len(series)
    x_bar = np.mean(series)
    series_centered = series - x_bar
    return np.dot(series_centered[:(n - h)], series_centered[h:]) / (n - h)

# INDEPENDENCE TESTING
def acorr_chi2_test(series, lags, alpha=0.05):
    n = len(series)
    quantile = chi2.ppf(q=1-alpha, df=lags)
    sum_of_autocov = 0

    for lag in range(1, lags + 1):
        sum_of_autocov += auto_covariance(series, lag)

    test_statistic = n * sum_of_autocov / (auto_covariance(series, 0) ** 2)

    if test_statistic > quantile:
        reject_h0 = True
    else:
        reject_h0 = False

    return quantile, test_statistic, reject_h0

def acorr_ljungbox_test(series, **kwargs):
    results = acorr_ljungbox(series, **kwargs)
    return results

# STATIONARITY TESTING

import numpy,from scipy,from statsmodels


# PREAMBLE:

import numpy as np
from numpy.linalg import *
import matplotlib.pyplot as plt

def pearson_correlation(trajectory_data):
        """ Calculates the Pearson Correlation Matrix for node pairs
        Usage: node_correlation, node_variance, node_average = pearson_correlation(trajectory_data)
        Arguments:
        trajectory_data: multidimensional numpy array; first index (rows) correspond to timestep, second index correspond to positions of each node;

        Returns:
        node_correlation: a nNodes x nNodes square matrix (numpy array) filled with the Pearson Correlation Coefficients for all node pairs
        node_variance: one dimensional numpy array containing the variances of the data
        node_average: one dimensional numpy array containing the averages of the data

        """

        # ----------------------------------------
        # CALCULATING THE AVERAGE OF TRAJECTORY DATA
        # ----------------------------------------
        nSteps = len(trajectory_data)
        nSteps_range = range(nSteps)
        nNodes = len(trajectory_data[0])
        nNodes_range = range(nNodes)
        for ts in nSteps_range:
                # removing center of geometry translational motion
                center_of_geometry = np.mean(trajectory_data[ts])
                trajectory_data[ts] -= center_of_geometry
                # no rotations to worry about...

        node_average = np.sum(trajectory_data,axis=0)/nSteps

        # ----------------------------------------
        # PREPARE NUMPY ARRAYS
        # ----------------------------------------
        node_variance = np.zeros(nNodes,dtype=np.float64)
        node_covariance = np.zeros((nNodes,nNodes),dtype=np.float64)
        node_correlation = np.zeros((nNodes,nNodes),dtype=np.float64)

        # ----------------------------------------
        # CALCULATING PEARSON CORRELATION COEFFICIENT MATRIX
        # ----------------------------------------
        for ts in nSteps_range:
                for i in nNodes_range:
                        node_variance[i] += trajectory_data[ts,i]**2
                        for j in nNodes_range[i:]:
                                node_covariance[i,j] += trajectory_data[ts,i]*trajectory_data[ts,j]

        node_variance /= nSteps
        node_variance -= node_average**2
        node_covariance /= nSteps

        for i in nNodes_range:
                for j in nNodes_range[i:]:
                        node_covariance[i,j] -= node_average[i]*node_average[j]
                        node_correlation[i,j] = node_covariance[i,j]/np.sqrt(node_variance[i]*node_variance[j])
                        node_correlation[j,i] = node_correlation[i,j]

        # ----------------------------------------
        # OUTPUT OF AVERAGE, VARIANCE, COVARIANCE, AND CORRELATION MATRICES
        # ----------------------------------------
        np.savetxt('node_positional_average.dat',node_average)
        np.savetxt('node_positional_variance.dat',node_variance)
        np.savetxt('node_positional_covariance.dat',node_covariance)
        np.savetxt('node_positional_correlation.dat',node_correlation)

        # ----------------------------------------
        # PLOTTING VARIANCE
        # ----------------------------------------
        plt.plot(nNodes_range,node_variance,'k')
        plt.xlabel('Node Index',size=14)
        plt.ylabel(r'Node Variance ($\AA^{2})',size=14)
        plt.tight_layout()
        plt.savefig('node_positional_variance.png',dpi=600,transparent=True)
        plt.close()

        # ----------------------------------------
        # PLOTTING COVARIANCE
        # ----------------------------------------
        fig, ax = plt.subplots()
        temp = plt.pcolormesh(nNodes_range,nNodes_range,node_covariance,cmap='Blues')
        cb1 = plt.colorbar()
        cb1.set_label(r'Node-Node Covariance ($AA^{2}$)')

        xlabels = [str(int(x)) for x in temp.axes.get_xticks()[:]]
        ylabels = [str(int(y)) for y in temp.axes.get_yticks()[:]]
        temp.axes.set_xticks(temp.axes.get_xticks(minor=True)[:]+0.5,minor=True)
        temp.axes.set_xticks(temp.axes.get_xticks()[:]+0.5)
        temp.axes.set_yticks(temp.axes.get_yticks(minor=True)[:]+0.5,minor=True)
        temp.axes.set_yticks(temp.axes.get_yticks()[:]+0.5)
        temp.axes.set_xticklabels(xlabels)
        temp.axes.set_yticklabels(ylabels)

        plt.xlim((-0.5,nNodes+0.5))
        plt.ylim((-0.5,nNodes+0.5))
        plt.xlabel('Node Index',size=14)
        plt.ylabel('Node Index',size=14)
        ax.set_aspect('equal')
        plt.tight_layout()
        plt.savefig('node_positional_covariance.png',dpi=600,transparent=True)
        plt.close()

        # ----------------------------------------
        # PLOTTING CORRELATION
        # ----------------------------------------
        fig, ax = plt.subplots()
        temp = plt.pcolormesh(nNodes_range,nNodes_range,node_correlation,cmap='bwr',vmin=-1.0,vmax=1.0)
        cb1 = plt.colorbar()
        cb1.set_label('Node-Node Correlation')

        xlabels = [str(int(x)) for x in temp.axes.get_xticks()[:]]
        ylabels = [str(int(y)) for y in temp.axes.get_yticks()[:]]
        temp.axes.set_xticks(temp.axes.get_xticks(minor=True)[:]+0.5,minor=True)
        temp.axes.set_xticks(temp.axes.get_xticks()[:]+0.5)
        temp.axes.set_yticks(temp.axes.get_yticks(minor=True)[:]+0.5,minor=True)
        temp.axes.set_yticks(temp.axes.get_yticks()[:]+0.5)
        temp.axes.set_xticklabels(xlabels)
        temp.axes.set_yticklabels(ylabels)

        plt.xlim((-0.5,nNodes+0.5))
        plt.ylim((-0.5,nNodes+0.5))
        plt.xlabel('Node Index',size=14)
        plt.ylabel('Node Index',size=14)
        ax.set_aspect('equal')
        plt.tight_layout()
        plt.savefig('node_positional_correlation.png',dpi=600,transparent=True)
        plt.close()

        return node_correlation, node_variance, node_average


## Sigmoid activation function

import numpy as np

class Sigmoid:
    def __init__(self):
        """
        ## This function is used to calculate the sigmoid and the derivative
            of a sigmoid
        """

    def forward(self, x):
        return 1.0 / (1.0 + np.exp(-x))

    def backward(self, x):
        return x * (1.0 - x)


import scipy.stats as scs

def min_sample_size(baseline_conversion_rate, minimum_detectable_effect, significance_level=0.05, min_power=0.8, size_ratio=1., two_tailed=True):

    if minimum_detectable_effect == 0:
        raise ValueError('Minimum detectable effect must be greater than 0%')
    if significance_level == 0:
        raise ValueError('The statistical significant level must be less than 100%')
    if significance_level == 1:
        raise ValueError('The statistical significant level must be greater than 0%')
    if min_power == 1:
        raise ValueError('The power must be less than 100%')
    if min_power == 0:
        raise ValueError('The power must be strictly greater than 0%')
    if size_ratio == 0:
        raise ValueError('Size ratio must be greater than 0%')

    Z_alpha = z_value(significance_level, two_tailed=two_tailed)
    Z_power = z_value(1-min_power, two_tailed=False)
    pooled_probability = (baseline_conversion_rate+size_ratio*(baseline_conversion_rate+minimum_detectable_effect))/(1+size_ratio)
    min_N_B = (1+size_ratio)*pooled_probability*(1-pooled_probability)*(Z_alpha+Z_power)**2/minimum_detectable_effect**2
    min_N_A = min_N_B/size_ratio
    return round(min_N_A), round(min_N_B)

def z_value(significance_level, two_tailed=True):
    z_dist = scs.norm()
    if two_tailed:
        significance_level = significance_level/2
        area = 1 - significance_level
    else:
        area = 1 - significance_level
    z = z_dist.ppf(area)
    return z


"""Estimate the confidence interval of a Bernoulli dist. parameter.

It is a conservative interval i.e. it overestimates the confidence
interval bounds.

Useful to find confidence intervals for machine learning models
accuracy.
"""
import scipy.stats
import numpy as np

def bernoulli_confidence_interval(
    accuracy: float, num_inst: int, confidence: float = 0.95
):
    crit_point = scipy.stats.norm.isf(0.5 * (1.0 - confidence))

    dist = 0.5 * crit_point / np.sqrt(num_inst)

    lower_bound = max(0.0, accuracy - dist)
    upper_bound = min(1.0, accuracy + dist)

    return (lower_bound, upper_bound)

def _test():
    accuracy = 0.98
    confidence = 0.95
    num_inst = 128

    print(bernoulli_confidence_interval(accuracy, num_inst, confidence))

if __name__ == "__main__":
    _test()


# -*- coding: utf-8 -*-

import numpy as np
from numpy import linspace, logspace, diff, zeros
from numpy import cos, sin, exp, log, pi
import matplotlib.pyplot as plt

####################################
# Implementación genérica de Euler
# f(t,x): derivada de x respecto al tiempo
# x0: condición inicial
# t0, tf: tiempo inicial y final
# h: paso de integración
####################################
def euler(f, x0, t0, tf, h):
    """
    Arguments:
        f: Function handler. Must recieve two arguments, t and x. f(t, x).
        x0: Initial value
        t0 / tf: Initial and final time to evaluate
        h: Desiered step

    Returns:
        t, x
        t: Array with the time points used for the function's calculation.
            (X axis on a plot)
            Shape: (n,)
        x: Obtained values at a given t.
            (Y axis on a plot)
            Shape: (n,1)  - One row for each element in t -
    """
    N = int((tf - t0) / h)  # número de puntos
    t = linspace(t0, tf, N + 1)
    n = x0.shape[0]  # dimensión del problema
    x = zeros((n, N + 1))
    x[:, 0] = x0
    for k in range(N):
        x[:, k + 1] = x[:, k] + h * f(t[k], x[:, k])
    return t, x

def taylor(x0, t0, tf, h, *derivatives):
    """
    Generic implementation of taylor of N order
    Arguments:
        x0: Initial value
        t0 / tf: Initial and final time to evaluate
        h: Desiered step
        f: derivatives of the function g, starting from g'
        up to the desired order

    Returns:
        t, x
        t: Array with the time points used for the function's calculation.
            (X axis on a plot)
            Shape: (n,)
        x: Obtained values at a given t.
            (Y axis on a plot)
            Shape: (n,1)  - One row for each element in t -
    """
    N = int((tf - t0) / h)  # número de puntos
    t = linspace(t0, tf, N + 1)
    n = x0.shape[0]  # dimensión del problema
    x = zeros((n, N + 1))
    x[:, 0] = x0
    for k in range(N):
        x[:, k + 1] = x[:, k]
        for index, der in enumerate(derivatives):
            i = index+1
            x[:, k + 1] = x[:, k + 1] + der(t[k], x[:, k]) * (h ** i) / np.math.factorial(i)

    return t, x

# LO SIGUIENTE ES EL EJEMPLO QUE DIÓ EN CLASE USANDO AL FUNCIÓN TAYOR
# SI CORRES LA FUNCIÓN PARA MOSTRAR EL ERROR TENES QUE CAMBIAR AHÍ LOS DATOS
# SEGUN EL GRAOD QUE USES
# SAME PLOTAYLOR

########################
# EJEMPLO
########################
R = 1e3  # Valor de la resistencia
C = 1e-6  # Valor de la capacidad
w = 2.0 * pi * 1000  # frecuencia angular de la señal de entrada
A = 1.0  # amplitud de la señal de entrada
T = 5 * 2 * pi / w  # simulo cinco ciclos

####################################
# Derivada primera de x
def dx(t, x):
    return ((A * cos(w * t) - x) / (R * C))

####################################
# Derivada segunda de x
def d2x(t, x):
    return ((-A * w * sin(w * t) - ((A * cos(w * t) - x) / (R * C))) / (R * C))

####################################
# Plot ejemplo
def plotaylor(h):
    x0 = zeros(1)
    t, xt = taylor(x0, 0, T, h, dx, d2x)
    fig, ax = plt.subplots()
    ax.plot(t, xt[0, :], label='x(t)')
    ax.legend()
    plt.title('Ejercicio')

    plt.show()

####################################
# Estimación error
def esterrorejemplo(h):

    # i es el grado del polinomio
    i = 2

    x0 = zeros(1)
    t, xt1 = taylor(x0, 0, T, h, dx, d2x)
    t, xt2 = taylor(x0, 0, T, h/2, dx, d2x)

    eet = abs(xt1[0, -1] - xt2[0, -1]) / ((2**i)-1)

    return eet

plotaylor(T / 10000)
print(esterrorejemplo(T / 10000))


import numpy as np

# 1 - Create your first array with the elements [1,22.4,5,35,4,6.7,3,8,40] and print it. Experiment what the
# following functions do: ndim, shape, size and dtype.
my_array = np.array([1, 22.4, 5, 35, 4, 6.7, 3, 8, 40])
print(my_array)
print(f'my_array.ndim is {my_array.ndim}')
print(f'my_array.shape is {my_array.shape}')
print(f'my_array.size is {my_array.size}')
print(f'my_array.dtype is {my_array.dtype}')

# 2 - Create your first matrix with the elements [['a', 'b'],['c', 'd'],[3, 3]] and print it. Experiment what the
# following functions do: ndim, shape, size and dtype
my_matrix = np.array([['a', 'b'], ['c', 'd'], [3, 3]])
print(my_matrix)
print(f'my_matrix.ndim is {my_matrix.ndim}')
print(f'my_matrix.shape is {my_matrix.shape}')
print(f'my_matrix.size is {my_matrix.size}')
print(f'my_matrix.dtype is {my_matrix.dtype}')

# 3 - Create numpy 1 dimension array using each of the functions arange and rand
range_array = np.arange(0, 10)
print(f'range_array is {range_array}')

random_array = np.random.rand(10)
print(f'random_array is {random_array}')

# 4 - Create numpy 2 dimensions matrix using each of the functions zeros and rand
zeros_matrix = np.zeros((5, 5))
print(f'range_array is {zeros_matrix}')

random_matrix = np.random.rand(4, 4)
print(f'random_array is {random_matrix}')

# 5 - Create an array containing 20 times the value 7. Reshape it to a 4 x 5 Matrix
result_array = np.ones(20) * 7
reshaped_array = result_array.reshape(4,5)
print(reshaped_array)

# 6 - Create a 6 x 6 matrix with all numbers up to 36, then print:
matrix_to_slice = np.arange(1, 37).reshape(6, 6)
print(matrix_to_slice)
print(f'only the first element on it: {matrix_to_slice[0,0]}')
print(f'only the last 2 rows for it: {matrix_to_slice[4:6,:]}')
print(f'only the two mid columns and 2 mid rows for it: {matrix_to_slice[2:4,2:4]}')
print(f'the sum of values for each column: {matrix_to_slice.sum(axis=0)}')


# -*- coding: utf-8 -*-
"""
Created on Sat Jan  6 19:51:06 2018

@author: Alankar
"""
import numpy as np
import matplotlib.pyplot as plt

def BE(A,Z):
    a1, a2, a3, a4 = 15.67, 17.23, 0.75, 93.2
    a5 = 0
    if not(A%2==0):
        a5 = 0
    elif(A%2==0 and Z%2==0):
        a5 = 12
    elif(A%2==0 and not(Z%2==0)):
        a5 = -12
    return a1*A - a2*A**(2/3) - a3*(Z**2/A**(1/3)) - a4*((A-2*Z)**2/A) + a5/A**(1/2)

print('-----------------------------------------------------------')
print('Demo Output: \nBinding Energy of a nucleus with Z = %d and A = %d is %.2f MeV'%(28,58,BE(58,28)))
print('Binding Energy per nucleon: %.2f MeV'%(BE(58,28)/58))
print('-----------------------------------------------------------')

Z = float(input('Enter Atomic number of nucleus: '))
A = float(input('Enter Mass number of nucleus: '))
B = BE(A,Z)
print('\nBinding Energy is %.2f MeV'%B)
print('Binding Energy per nucleon: %.2f MeV'%(BE(A,Z)/A))

def stableA(Z):
    B = BE(Z,Z)/Z
    A_st = Z
    for A in range(Z+1,3*Z+1):
        B_tmp = BE(A,Z)/A
        if (B_tmp>B):
            B = B_tmp
            A_st = A
    return (A_st,B)

NuProf = np.zeros((100,2),dtype=np.float32)
for Z in range(0,100):
    st = stableA(Z+1)
    NuProf[Z,0] = st[0]
    NuProf[Z,1] = st[1]
    print('Most stable nucleus with Z = %d has A = %d'%((Z+1),NuProf[Z,0]))

print('\n\nMax Binding Energy per nucleon is at Z = ',(np.argmax(NuProf[:,1])+1))

plt.plot(np.arange(1,101,1),NuProf[:,1])
plt.xlabel('Atomic Number Z')
plt.ylabel('Binding Energy per nucleon (MeV)')
plt.savefig('Binding Energy Curve.png')
plt.show()


import numpy as py
np.random.see(42)

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

def sigmoid_prime(x):
	return sigmoid(x) * (1-sigmoid(x))

def prediction(X, W, b):
	prediction_ = sigmoid(np.matul(X,W)+b)
	return prediction_

def error_vector(y, y_hat):
	errors = [-y[i]*np.log(y_hat[i]) - (1-y[i]) * np.log(1-y_hat[i]) for i in range(len(y))]
	return errors

def error(y, y_hat):
	ev = error_vector(y, y_hat)
	return sum(ev)/len(ev)

def dErrors(X, y, y_hat):
	DErrorsDx1 = [-X[i][0]*(y[i]-y_hat[i]) for i in range(len(y))]
	DErrorsDx2 = [-X[i][1]*(y[i]-y_hat[i]) for i in range(len(y))]
	DErrprsDb =[-(y[i]-y_hat[i]) for i in range(len(y))]
	return DErrorsDx1, DErrorsDx2, DErrorsDb

def gradientDescentStep(X, y, W, b, learn_rate =0.01)
	y_hat = prediction(X, W, b)
	errors = error_vector(y, y_hat)
	derivErrors = dErrors(X, y, y_hat)
	W[0] -=sum(derivErrors[0])*learn_rate
	W[1] -=sum(derivErrors[1])*learn_rate
	b -=sum(derivErrors[2])*learn_rate
	return W, b, sum(errors)

def trainLR(X, y, learn_rate=0.01, num_epochs =100):
	x_min, x_max = min(X.T[0]), max(X.T[0])
	y_min, y_max = min(X.T[1]), max(X.T[1])
	W = np.array(np.random.rand(2,1))*2 -1
	b = np.random.rand(1)[0]*2 -1
	boundary_lines =[]
	errors =[]
	for i in range(num_epochs):
		W, b, error = gradientDescentStep(X, y, W, b, learn_rate)
		boundary_lines.append((-W[0]/W[1], -b/W[1]))
		errors.append(error)
	return boundary_lines, erros

lesson 4 Intro to Neural Networks/logistic_regression.py


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

trial = 100000

init = 0.0
states = [init]

for i in range(trial):
    state = states[-1]
    # sample from uniform(state-1, state+1)
    y = random.uniform(state - 1.0, state + 1.0)
    # accept the proposal with probability min(1.0, e^(-0.5*(y^2 - s^2)))
    p = min(1.0, math.exp(-0.5 * (y**2 - state**2)))
    new = np.random.choice([state, y], p=[1-p, p])

    states.append(new)

plt.hist(states, 50, density=True)
plt.title('simulation results of standard normal distribution')
plt.savefig('standard_normal_distribution.png', dpi=300)
plt.show()

means, variances = [], []
step = trial // 1000
states = np.array(states)
for i in range(1000):
    end = step * (i+1)
    mean = np.mean(states[:end])
    variance = np.var(states[:end])
    means.append(mean)
    variances.append(variance)
means = np.array(means)
variances = np.array(variances)

plt.figure(figsize=(12, 6))
plt.subplot(121)
plt.plot(range(1000), np.abs(0.0-means))
plt.title('convergence rate of mean')
plt.xlabel("iteration (* %d)"%(step))
plt.ylabel("approximation error")

plt.subplot(122)
plt.plot(range(1000), np.abs(1.0-variances))
plt.title("convergence rate of variance")
plt.xlabel("iteration (* %d)"%(step))

plt.ylabel("approximation error")
plt.savefig("standard_normal_distribution_convergence_rate.png", dpi=300)
plt.show()


#reason for going to robust regression- RANSAC
#http://digitalfirst.bfwpub.com/stats_applet/stats_applet_5_correg.html

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import RANSACRegressor

#Read raw csv file without headers and comma delimiter
df=pd.read_csv('housing.data',delim_whitespace=True,header=None)

#give names for each column
col_name=['CRIM','ZIN','INDUS','CHAS','NOX','RM','AGE','DIS','RAD','TAX','PIRATIO','B','LSTAT','MEDV']
df.columns=col_name

#Select the input and output datas
X=df['RM'].values.reshape(-1,1)
y=df['MEDV'].values

#fitting the data with RANdom SAmple Consensus (RANSAC) Algorithm
ransac=RANSACRegressor()
ransac.fit(X,y)

#saperating inler and outlier points
inlier_mask=ransac.inlier_mask_
outlier_mask=np.logical_not(inlier_mask)

#predicting the values with (3,4,5,6,7,8,9)
line_X=np.arange(3,10,1)
line_y_ransac=ransac.predict(line_X.reshape(-1,1))

#plot the figure
sns.set(style='darkgrid',context='notebook')
plt.figure(figsize=(12,10));
plt.scatter(X[inlier_mask],y[inlier_mask],c='blue',marker='o',label='Inliers')
plt.scatter(X[outlier_mask],y[outlier_mask],c='brown',marker='s',label='Outliers')
plt.plot(line_X,line_y_ransac,color='red')
plt.xlabel('RM')
plt.ylabel('MEDV')
plt.legend(loc='upper left')
plt.show()

#check and compare the coef and intercept with linear reg
print(ransac.estimator_.coef_,ransac.estimator_.intercept_)


# -*- coding: utf-8 -*-
# @Time    : 7/31/2018 7:29 PM
# @Author  : DonaldY

# 生成 10 * 10的希尔伯特矩阵
from scipy.linalg import hilbert

x = hilbert(10)
x

# 希尔伯特转置矩阵于原矩阵相乘
import numpy as np

np.matrix(x).T * np.matrix(x)


import sympy
import csv
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from sympy.plotting.plot import List2DSeries
from timeit import default_timer as timer
import random

matplotlib.style.use('bmh')
#seaborn-whitegrid
#fivethirtyeight

def Lagrange (Lx, Ly):
    X = sympy.symbols('X')
    if  len(Lx) != len(Ly):
        print ("Error data set")
        return 1
    y = 0
    for i in range(len(Lx)):
        t = 1
        for j in range(len(Lx)):
            if j != i:
                t *= ((X - Lx[j]) / (Lx[i] - Lx[j]))
        y += t * Ly[i]
    return y

GINI_KNOWN = 0.293 # here is Croatia's gini in 2018
Lx = []
Ly = []
yPos = []

Ly.append(0.0)
with open('input_hrv.csv', 'r') as file:
    reader = csv.reader(file)
    for row in reader:
        r = 0
        for value in row:
            r += 1
            try:
                value = float(value)
                if r % 2 == 0:
                    Ly.append(value)
            except ValueError as e:
                continue
GDP = Ly[-1]
del Ly[-1]
Ly.sort()
for i in range(len(Ly)):
    Lx.append(i * 10.0)

print("---------- Dependencies ----------")
print('matplotlib: {}'.format(matplotlib.__version__))
print('sympy: {}'.format(sympy.__version__))
print('numpy: {}'.format(np.__version__))
print("----------------------------------")

print("----------- INPUT DATA -----------")
print("X Data: ", Lx)
print("Y Data: ", Ly)
print("Total Income: ", GDP)
print("----------------------------------")

print("----------- SET POINTS -----------")
for i in range(len(Lx)):
    yValue = (Ly[i] * 1.0) / (GDP * 1.0) * 100.0
    if i != 0:
        yValue += yPos[i - 1]
    print("X: ", Lx[i], "Y: ", yValue)
    yPos.append(yValue)
print("----------------------------------")

print("----------- INPUT DATA ------------")
print("X Vector: ", Lx)
print("Y Vector: ", yPos)
print("-----------------------------------")

X = sympy.symbols('X')
xLimit = 100

startL = timer()
MyLagrange = Lagrange(Lx, yPos)
lorenz = sympy.simplify(MyLagrange)
equality = X
areaEquality = sympy.integrate(equality, (sympy.Symbol('X'), 0, xLimit))
areaGINI = sympy.integrate(equality - lorenz, (sympy.Symbol('X'), 0, xLimit))
lagGINI = areaGINI / areaEquality
endL = timer()

timeLagrange = endL - startL
lagAccuracy = abs(100.0 - (abs(GINI_KNOWN - lagGINI) / GINI_KNOWN) * 100.0)

print("------------ LAGRANGE ------------")
print("Lagrange Polynomial:", lorenz)
print("Object Type:", type(lorenz))
print("Enclosed Area:", areaGINI)
print("Equality Area:", areaEquality)
print("GINI Coefficient:", lagGINI)
print("Time:", timeLagrange, "seconds")
print("Accuracy: {:0.2f} %".format(lagAccuracy))
print("----------------------------------")

print("+++++++++++++ INTEGRATION ++++++++++++")
delta = 0.005
acc = 0.0
accArea = 0.0
eqArea = 0.0

startR = timer()
while acc <= xLimit:
    acc += delta
    eval = acc - (delta / 2.0)
    heightEq = equality.evalf(subs={X:eval})
    heightLorenz = lorenz.evalf(subs={X:eval})
    eqArea += delta * (heightEq)
    accArea += delta * (heightEq - heightLorenz)
endR = timer()
timeRiemann = endR - startR
rieGINI = accArea / eqArea
rieAccuracy = abs(100.0 - (abs(GINI_KNOWN - rieGINI) / GINI_KNOWN) * 100.0)

print("------------ RIEMANN ------------")
print("Enclosed Area:", accArea)
print("Equality Area:", eqArea)
print("GINI Coefficient:", rieGINI)
print("Time:", timeRiemann, "seconds")
print("Accuracy: {:0.2f} %".format(rieAccuracy))
print("----------------------------------")

tests = 50000
inside = 0.0
areaDomain = 100 * 100
insidePoints = []
outsidePoints = []

# For Monte Carlo visualization
'''
xx = np.linspace(0, 100, 1000)
yy = sympy.lambdify(X, [equality, lorenz])(xx)
plt.plot(xx, np.transpose(yy))
'''

startMC = timer()
for i in range(tests):
    xCoord = random.uniform(0, 100)
    yCoord = random.uniform(0, 100)

    if yCoord <= equality.evalf(subs={X:xCoord}) and yCoord >= lorenz.evalf(subs={X:xCoord}):
        inside += 1.0
        point = [xCoord, yCoord]
        insidePoints.append(point)
        #plt.plot(point[0], point[1], color='red', marker='o')
    '''
    # For Monte Carlo visualization
    else:
        point = [xCoord, yCoord]
        outsidePoints.append(point)
        plt.plot(point[0], point[1], color='blue', marker='o')

    if i == 10 or i == 50 or i == 100 or i == 500 or i == 1000 or i == 5000:
        plt.title('T = {}'.format(i))
        plt.savefig("T_{}.png".format(i),bbox_inches='tight')
    '''
endMC = timer()

timeMC = endMC - startMC
MonteCarloArea = (inside / tests) * areaDomain
mcGINI = MonteCarloArea / areaEquality
mcAccuracy = abs(100.0 - (abs(GINI_KNOWN - mcGINI) / GINI_KNOWN) * 100.0)
print("------------ MONTE CARLO ------------")
print("Enclosed Area:", MonteCarloArea)
print("GINI Coefficient:", mcGINI)
print("Time:", timeMC, "seconds")
print("Accuracy: {:0.2f} %".format(mcAccuracy))
print("------------------------------------")

# Plots
p1 = sympy.plotting.plot(lorenz, equality, (X, 0, xLimit), ylim=[0,100],show=False)
p1.title = "Lagrange Interpolation for GINI Approximation"
p1[0].line_color='r'

p2 = sympy.plotting.plot(lorenz, equality, (X, 0, xLimit), ylim=[0,100],show=False)
p2.title = "Lagrange Interpolation vs. Data Set Comparison"
p2.append(List2DSeries(Lx, yPos))
p2[2].line_color=(0.5, 0.5, 0.5)
p2[0].line_color='r'

# Time Performance visualization
finalTime = [timeLagrange, timeMC, timeRiemann]
labels = ["Python", "Monte Carlo", "Riemann"]
index = np.arange(len(labels))
viridis = matplotlib.cm.get_cmap('viridis', 5)
bars = plt.bar(index, finalTime, color=viridis.colors[2])
plt.xlabel('Method for Polynomial Integration', fontsize=13)
plt.ylabel('Time (s)', fontsize=13)
plt.xticks(index, labels, fontsize=8, rotation=0)
plt.title('Time Performance Comparison')

'''
# For Riemann visualization
xx = np.linspace(0, 100, 1000)
yy = sympy.lambdify(X, [equality, lorenz])(xx)
plt.plot(xx, np.transpose(yy))

f = sympy.lambdify(X, lorenz)
a = 0; b = 100; N = 10
n = 10
x = np.linspace(a,b,N+1)
y = f(x)
x_mid = (x[:-1] + x[1:])/2 # midpoints
y_mid = f(x_mid)
plt.bar(x_mid,y_mid,width=(b-a)/N,alpha=0.2,edgecolor='b')
#plt.title('Midpoint Riemann Sum, N = {}'.format(N))
'''

p1.show()
p2.show()
plt.show()


from sympy.abc import r
from sympy import *

"""
N-dimensional volume of a sphere derivation:

ND cube volume = r^n
ND sphere volume = C*r^n
where C is a proportionality constant, we need to derive
the equation for C.

Let V_n(r) be a function that returns the volume of a sphere in ND

The surface is then
dV_n(r)/dr = n * C_n * r^(n-1)

hence the volume of the elements are
dV_n(r)/dr * dr = n * C_n * r^(n-1) * dr

Set r^2 to be t and we can see the following relation to the gamma function

Cn = pi^(n/2) / Gamma(0.5*n + 1)

Use recurrence relation and get
C(n) = 2pi/n * C(n-2)

as n -> infinity, C(n) -> 0, this means that the volume of a sphere approaches 0
as the dimension increases, this tells you that the all the volume exists on its surface
"""

pi = Symbol('pi')

def C(n):
    if n <= 0:
        return 0

    # we can think of a 1d sphere as a line, so its area is the positive and the negative part
    # of the line, hence why it is 2
    if n == 1:
        return 2

    if n == 2:
        return pi

    return 2*pi/n * C(n-2)

def V(n):
    return simplify(r**n * C(n))

for i in range(1, 101):
    print(i, V(i))


import numpy as np
import matplotlib.pyplot as plt

## softmax: 0.1 600
## perceptron: 0.05 550

def readData(csvname):
    data = np.loadtxt(csvname, delimiter=',')
    x = data[:-1, :]
    y = data[-1:, :]

    return x, y

def softmaxCostFunc(x, y, w):
    cost = np.sum(np.log(1 + np.exp(-y*np.transpose(np.dot(np.transpose(x), w)))))
    return cost / float(np.size(y))

def gradientDescentOneStepForSoftmax(x, y, w, alpha=0.1):
    total = np.zeros([9,1])
    for i in range(np.size(y)):
        power = np.exp(-y[:,i] * np.dot(x[:,i], w))
        term = power / (1 + power)
        total += term * y[:,i] * x[:,[i]]

    w = w + alpha * (1/np.size(y)) * total
    return w

def perceptronCostFunc(x, y, w):
    cost = 0
    a = (-y*np.transpose(np.dot(np.transpose(x), w)))[0]
    for i in range(len(a)):
        cost += a[i] if (a[i] > 0) else 0
    return cost / float(np.size(y))

def gradientDescentOneStepForPerceptron(x, y, w, alpha=0.05):
    total = np.zeros([9,1])
    for i in range(np.size(y)):
        term = -y[:,i] * np.dot(x[:,[i]].T, w)
        total += 0 if term <= 0 else -y[:,i] * x[:,[i]]

    w = w - alpha * (1/np.size(y)) * total
    return w

if  __name__  ==  "__main__":
    csvname = 'breast_cancer_data.csv'
    x, y = readData(csvname)
    w = np.ones([x.shape[0] + 1, 1])
    x = np.insert(x, 0, values=np.ones([1, x.shape[1]]), axis=0)

    xSoftList = [0]
    ySoftList = [softmaxCostFunc(x, y, w)]
    for i in range(600):
        w = gradientDescentOneStepForSoftmax(x, y, w)
        xSoftList.append(i+1)
        ySoftList.append(softmaxCostFunc(x, y, w))

    yPredic = np.transpose(np.dot(np.transpose(x), w))
    wrong = 0
    for i in range(np.size(yPredic)):
        if ((yPredic[0][i] > 0) != (y[0][i] > 0)):
            wrong += 1
    print("Softmax Wrong Prediction: ", wrong)

    w = np.ones([x.shape[0], 1])
    xPerceptronList = [0]
    yPerceptronList = [perceptronCostFunc(x, y, w)]
    for i in range(550):
        w = gradientDescentOneStepForPerceptron(x, y, w)
        xPerceptronList.append(i+1)
        yPerceptronList.append(perceptronCostFunc(x, y, w))

    plt.plot(xSoftList, ySoftList, label="Softmax Cost Function",color="#F08080")
    plt.plot(xPerceptronList, yPerceptronList, label="Perceptro Cost Function")
    plt.legend(loc="upper right")
    plt.show()
    plt.close()

    yPredic = np.transpose(np.dot(np.transpose(x), w))
    wrong = 0
    for i in range(np.size(yPredic)):
        if ((yPredic[0][i] > 0) != (y[0][i] > 0)):
            wrong += 1
    print("Perceptron Wrong Prediction: ", wrong)


import numpy as np

def mobius_add(u, v):
    norms_u = np.sum(np.power(u, 2), -1, keepdims=True)
    norms_v = np.sum(np.power(v, 2), -1, keepdims=True)
    inner_product = np.sum(u * v, -1, keepdims=True)
    denominator = 1 + 2 * inner_product + norms_u * norms_v
    numerator = (1 + 2 * inner_product + norms_v) * u + (1 - norms_u) * v
    results = numerator / denominator
    return results

def compute_hyperbolic_distances(vectors_u, vectors_v):
    """
    Compute poincare distances between input vectors.
    Modified based on gensim code.
    vectors_u: (batch_size, dim)
    vectors_v: (batch_size, dim)
    """
    euclidean_dists = np.linalg.norm(vectors_u - vectors_v, axis=1)  # (batch_size, )
    norms_u = np.linalg.norm(vectors_u, axis=1)  # (batch_size, )
    norms_v = np.linalg.norm(vectors_v, axis=1)  # (batch_size, )
    alpha = 1 - norms_u ** 2  # (batch_size, )
    beta = 1 - norms_v ** 2  # (batch_size, )
    gamma = 1 + 2 * ((euclidean_dists ** 2) / (alpha * beta))  # (batch_size, )
    poincare_dists = np.arccosh(gamma + 1e-10)  # (batch_size, )
    return poincare_dists  # (batch_size, )
    # distance = 2 * np.arctanh(np.linalg.norm(mobius_add(vectors_u, vectors_v), axis=1))
    # return distance

def compute_hyperbolic_similarity(embeds1, embeds2):
    embeds1 = embeds1.cpu()
    embeds2 = embeds2.cpu()
    x1, y1 = embeds1.shape  # <class 'numpy.ndarray'>
    x2, y2 = embeds2.shape
    assert y1 == y2
    dist_vec_list = list()
    for i in range(x1):
        embed1 = embeds1[i, :]  # <class 'numpy.ndarray'> (y1,)
        embed1 = np.reshape(embed1, (1, y1))  # (1, y1)
        embed1 = np.repeat(embed1, x2, axis=0)  # (x2, y1)
        dist_vec = compute_hyperbolic_distances(embed1, embeds2)
        dist_vec_list.append(dist_vec)
    dis_mat = np.row_stack(dist_vec_list)  # (x1, x2)
    # return (-dis_mat)
    # return np.exp(-dis_mat)  # sim mat
    return normalization(-dis_mat)

def normalization(data):
    _range = np.max(data) - np.min(data)
    return (data - np.min(data)) / _range


import numpy as np

def euclidean_distance(v1, v2, axis=0):
    return np.sqrt(euclidean_squared_distance(v1, v2, axis))

def euclidean_squared_distance(v1, v2, axis=0):
    return np.sum((v1 - v2) ** 2, axis=axis)

def manhattan_distance(v1, v2, axis=0):
    return np.sum(np.abs(v1 - v2), axis=axis)


"""
Created on Apr 14 23:59:42 2022
"""
from typing import Union

import numpy as np

np_arr = np.ndarray

FloatStr = Union[float, str]
FloatStrArr = Union[float, str, np_arr]

def change_instance(in_obj: FloatStr, in_type: str = 'dms') -> float:
    """
    Check if given input is DMS/HMS string and convert it to float.

    Parameters
    ----------
    in_obj : FloatStr
        Input object as HMS/DMS.
    in_type : str, optional
        Type of conversion. The default is 'dms'.

    Returns
    -------
    float
        DESCRIPTION.

    """
    if isinstance(in_obj, str):
        out = dms2dd(in_obj) if in_type == 'dms' else hms2dd(in_obj)
    else:
        out = in_obj

    return out

def altitude2zenith(altitude: FloatStrArr, deg_rad: bool = True) -> float:
    """
    Convert the given altitude to its complementary zenith angle.

    Parameters
    ----------
    altitude : FloatStrArr
        Altitude of the given celestial object.
    deg_rad : bool, optional
        Whether the given altitude measurement is in degrees or radians. Default is True.

    Returns
    -------
    float
        Complementary zenith angle for the corresponding altitude angle.

    """

    altitude = change_instance(altitude, 'dms')

    return 90 - altitude if deg_rad else np.pi / 2 - altitude

def zenith2altitude(zenith_angle: FloatStrArr, deg_rad: bool = True) -> float:
    """
    Convert the given zenith angle to its complementary altitude angle.

    Parameters
    ----------
    zenith_angle : FloatStrArr
        Zenith angle of the given celestial object.
    deg_rad : bool, optional
        Whether the given zenith angle measurement is in degrees or radians.
        The default is True.

    Returns
    -------
    float :
        Complementary altitude angle for the corresponding zenith angle.

    """

    zenith_angle = dms2dd(zenith_angle) if type(zenith_angle) == str else zenith_angle

    return 90 - zenith_angle if deg_rad else np.pi / 2 - zenith_angle

def dms2dd(dms: str) -> float:
    """
    Convert given degree minute second to degree decimal format.

    Parameters
    ----------
    dms : str
        String representing the degree-minute-second value.

    Returns
    -------
    float
        Degree decimal equivalent of the DMS input.

    Notes
    -------
        List conversion is possible

    """

    deg, minute, sec = [float(j) for j in dms.split(':')]

    # check for negative degree value
    if deg < 0:
        minute, sec = float(f'-{minute}'), float(f'-{sec}')

    return deg + minute / 60 + sec / 3600

def dd2dms(degree_decimal: float) -> str:
    """
    Convert given degree decimal format to degree minute seconds.

    Parameters
    ----------
    degree_decimal : float
        Degree decimal value.

    Returns
    -------
    str
        DMS equivalent of the input degree decimal value.

    """

    # get the truncated value
    _d = np.trunc(degree_decimal)

    # get the residual
    _deg_residual = abs(degree_decimal - _d)

    # get minutes
    __min = _deg_residual * 60
    _m = np.trunc(__min)

    # get the residual
    _min_residual = abs(__min - _m)

    # get seconds
    _s = round(_min_residual * 60, 4)

    _m, _s = [_m + 1, '00'] if _s == 60 else [_m, _s]

    if int(_s) == _s:
        _s = int(_s)

    return f'{int(_d)}:{int(_m)}:{_s}'

def hms2dd(hms: str) -> float:
    """
    Convert a given hour-minute-second string to degree decimal.

    Parameters
    ----------
    hms : str
        String representing the hour-minute-second value.

    Returns
    -------
    float
        Degree decimal value of the corresponding HMS input value.

    """

    hour, minute, sec = [float(i) for i in hms.split(':')]

    if hour < 0:
        print('RA value cannot be negative, assuming positive.')
        hour = -hour

    return hour * 15 + (minute / 4.) + (sec / 240.)

def dd2hms(degree_decimal: float) -> str:
    """
    Convert degree decimal to its corresponding HMS notation.

    Parameters
    ----------
    degree_decimal : float
        Degree decimal value for the position of the object.

    Returns
    -------
    str
        Corresponding HMS value for the input DD value.

    """

    if degree_decimal < 0:
        print('dd for HMS conversion cannot be negative, assuming positive.')
        _dd = -degree_decimal / 15
    else:
        _dd = degree_decimal / 15

    # get the truncated value
    _d = np.trunc(_dd)

    # get the residual
    _deg_residual = abs(_dd - _d)

    # get minutes
    __min = _deg_residual * 60
    _m = np.trunc(__min)

    # get the residual
    _min_residual = abs(__min - _m)

    # get seconds
    _s = round(_min_residual * 60, 4)

    _m, _s = [_m + 1, '00'] if _s == 60 else [_m, _s]

    if int(_s) == _s:
        _s = int(_s)

    return f'{int(_d)}:{int(_m)}:{_s}'

def RA2HA(right_ascension: FloatStr, local_time: FloatStr) -> str:
    """
    Converts right ascension to its corresponding hour angle value depending upon the
    given local time.

    Parameters
    ----------
    right_ascension : FloatStr
        Right ascension value for the celestial object. It can either be a string with
        HH:MM:SS format or a float number representing the right ascension value.
    local_time : FloatStr
        Local time for the observer.

    Returns
    -------
    str
        Hour angle value of the object in the sky according to observer's local time.

    """

    _ra = change_instance(right_ascension, 'hms')
    _lt = change_instance(local_time, 'hms')

    if _ra > _lt:
        _lt += 360

    return dd2hms(_lt - _ra)

def HA2RA(hour_angle: FloatStrArr, local_time: FloatStr) -> str:
    """
    Converts hour angle to its corresponding right ascension value depending upon the
    given local time.

    Parameters
    ----------
    hour_angle : FloatStr
        Hour angle value for the celestial object.
    local_time : FloatStr
        Local time for the observer.

    Returns
    -------
    str
        Right ascension value of the object in the sky according to observer's local time.

    Notes
    -------
        The hour_angle and local_time parameters can either be a string with HH:MM:SS
        format or a float number representing the degree decimal representation of
        their values.
    """

    _ha = change_instance(hour_angle, 'hms')
    _lt = change_instance(local_time, 'hms')

    if _ha > _lt:
        _lt += 360

    return dd2hms(_lt - _ha)


"""
Compositional Data?
--------------------
"""

########################################################################################
# pyrolite comes with a few datasets from Aitchison (1984) built in which we can use
# as examples:
#
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pyrolite.plot import pyroplot
from pyrolite.data.Aitchison import load_kongite

df = load_kongite()
########################################################################################
# For compositional data, everything is relative (thanks to the closure property), so we
# tend to use ratios to express differences or changes between things. However,
# if we make incorrect assumptions about the nature of our data, we can get some incorrect
# answers. Say you want to know the average ratio between A and B:
#
A_on_B = df["A"] / df["B"]
A_on_B.mean()  # 2.8265837788402983
########################################################################################
# Equally, you could have chosen to calculate the average ratio between B and A
#
B_on_A = df["B"] / df["A"]
B_on_A.mean()  # 0.4709565704852008
########################################################################################
# You expect these to be invertable, such that A_on_B = 1 / B_on_A; but not so!
#
A_on_B.mean() / (1 / B_on_A.mean())  # 1.3311982026717262
########################################################################################
# Similarly, the relative variances are different:
#
np.std(A_on_B) / A_on_B.mean()  # 0.6295146309597085
np.std(B_on_A) / B_on_A.mean()  # 0.5020948201979953
########################################################################################
# This improves when using logratios in place of simple ratios, prior to exponentiating means
#
logA_on_B = (df["A"] / df["B"]).apply(np.log)
logB_on_A = (df["B"] / df["A"]).apply(np.log)
########################################################################################
# The logratios are invertible:
#
np.exp(logA_on_B.mean())  # 2.4213410747400514
1 / np.exp(logB_on_A.mean())  # 2.421341074740052
########################################################################################
# The logratios also have the same variance:
(np.std(logA_on_B) / logA_on_B.mean()) ** 2  # 0.36598579018127086
(np.std(logB_on_A) / logB_on_A.mean()) ** 2  # 0.36598579018127086

########################################################################################
# These peculiarities result from incorrect assumptions regarding the distribution of the
# data: ratios of compositional components are typically *lognormally* distributed, rather
# than *normally* distributed, and the compositional components themselves commonly have
# a `Poisson distribution <https://en.wikipedia.org/wiki/Poisson_distribution>`__ .
# These distributions contrast significantly with the normal distribution at the core of
# most statistical tests. We can compare distributions with similar means and variances
# but different forms, and note that the normal distribution has one immediate
# failure, in that it has non-zero probability density below 0, and we know that you can't
# have negative atoms!
#
from scipy.stats import norm, poisson, lognorm

means = [[10, 10], [10, 20], [20, 100], [1000, 50]]
fig, ax = plt.subplots(len(means), 4, figsize=(11, 8))
ax[0, 0].set_title("A")
ax[0, 1].set_title("B")
ax[0, 2].set_title("Normal Fit to B/A")
ax[0, 3].set_title("Lognormal Fit to B/A")
ax[-1, 0].set_xlabel("A")
ax[-1, 1].set_xlabel("B")
ax[-1, 2].set_xlabel("B/A")
ax[-1, 3].set_xlabel("B/A")
for ix, (m1, m2) in enumerate(means):
    p1, p2 = poisson(mu=m1), poisson(mu=m2)
    y1, y2 = p1.rvs(2000), p2.rvs(2000)
    ratios = y2[y1 > 0] / y1[y1 > 0]

    y1min, y1max = y1.min(), y1.max()
    y2min, y2max = y2.min(), y2.max()
    ax[ix, 0].hist(
        y1,
        color="0.5",
        alpha=0.6,
        label="A",
        bins=np.linspace(y1min - 0.5, y1max + 0.5, (y1max - y1min) + 1),
    )
    ax[ix, 1].hist(
        y2,
        color="0.5",
        alpha=0.6,
        label="B",
        bins=np.linspace(y2min - 0.5, y2max + 0.5, (y2max - y2min) + 1),
    )

    # normal distribution fit
    H, binedges, patches = ax[ix, 2].hist(
        ratios, color="Purple", alpha=0.6, label="Ratios", bins=100
    )
    loc, scale = norm.fit(ratios, loc=0)
    pdf = norm.pdf(binedges, loc, scale)
    twin2 = ax[ix, 2].twinx()
    twin2.set_ylim(0, 1.1 * np.max(pdf))
    twin2.plot(binedges, pdf, color="k", ls="--", label="Normal Fit")

    # log-normal distribution fit
    H, binedges, patches = ax[ix, 3].hist(
        ratios, color="Green", alpha=0.6, label="Ratios", bins=100
    )
    s, loc, scale = lognorm.fit(ratios, loc=0)
    pdf = lognorm.pdf(binedges, s, loc, scale)
    twin3 = ax[ix, 3].twinx()
    twin3.set_ylim(0, 1.1 * np.max(pdf))
    twin3.plot(binedges, pdf, color="k", ls="--", label="Lognormal Fit")

    for a in [*ax[ix, :], twin2, twin3]:
        a.set_yticks([])

plt.tight_layout()
########################################################################################
# The form of these distributions is a reflection of the fact that geochemical data
# is at is core a measure of relative quantities of atoms. Quantities of atoms have
# discrete distributions (i.e. you can have precisely 0, 1 or 6.02 x 10^23 atoms, but 1.23
# atoms is not a sensible state of affairs); if you were to count them in a shiny
# machine, the amount of atoms you might measure over a given period will have a Poisson
# distribution. If you measure two components, the probability density distribution of the
# ratio is well approximated by a lognormal distribution (note this doesn't consider
# inherent covariance):
#
from pyrolite.util.plot.axes import share_axes, subaxes
from pyrolite.util.distributions import lognorm_to_norm, norm_to_lognorm

# starting from a normal distribution, then creating similar non-normal distributions
mean, sd = 2.5, 1.5  #
logmu, logs = norm_to_lognorm(mean, sd)  # parameters for equival
normrv = norm(loc=mean, scale=sd)
lognormrv = lognorm(s=logs, scale=logmu)
poissonrv = poisson(mu=mean)
########################################################################################
# We can visualise the similarities and differences between these distributions:
#
fig, ax = plt.subplots(2, 3, figsize=(8, 4))
ax = ax.flat
for a in ax:
    a.subax = subaxes(a, side="bottom")

share_axes(ax[:3], which="x")
share_axes(ax[3:], which="x")
ax[0].set_xlim(-2, 10)
ax[3].set_xscale("log")
ax[3].set_xlim(0.1, 10)

for a in ax:
    a.axvline(0, color="k", lw=0.5, ls="--")

# xs at which to evaluate the pdfs
x = np.linspace(-5, 15.0, 1001)

for ix, dist in enumerate([normrv, lognormrv, poissonrv]):
    _xs = dist.rvs(size=10000)  # random sample
    _ys = -0.05 + np.random.randn(10000) / 100  # random offsets for visualisation
    for a in [ax[ix], ax[ix + 3]]:
        a.annotate(
            "mean={:.2f}, var={:.2f}".format(np.mean(_xs), np.var(_xs)),
            xy=(0.05, 1.05),
            ha="left",
            va="bottom",
            xycoords=a.transAxes,
        )
        a.subax.scatter(_xs, _ys, s=2, color="k", alpha=0.01)
        if dist != poissonrv:  # cont. distribution
            a.plot(x, dist.pdf(x), color="Purple", alpha=0.6, label="pdf")
        else:  # discrete distribution
            a.vlines(
                x[x >= 0],
                0,
                dist.pmf(x[x >= 0]),
                color="Purple",
                alpha=0.6,
                label="pmf",
            )

fig.suptitle("Data Distributions: Normal, Lognormal, Poisson", y=1.1)
plt.tight_layout()
########################################################################################
# Accounting for these inherent features of geochemical data will allow you to
# accurately estimate means and variances, and from this enables the use of
# standardised statistical measures - as long as you're log-transforming your data.
# When performing multivariate analysis, use log-ratio transformations (including the
# additive logratio :func:`~pyrolite.comp.codata.alr`, centred logratio
# :func:`~pyrolite.comp.codata.clr` and isometric logratio
# :func:`~pyrolite.comp.codata.ilr`). In this case, the logratio-mean is implemented for
# you:
#
from pyrolite.comp.codata import logratiomean
import itertools

fig, ax = plt.subplots(2, 2, figsize=(12, 12), subplot_kw=dict(projection="ternary"))
ax = ax.flat

for columns, a in zip(itertools.combinations(["A", "B", "C", "D"], 3), ax):
    columns = list(columns)

    df.loc[:, columns].pyroplot.scatter(
        ax=a, color="k", marker=".", label=df.name, no_ticks=True
    )

    df.mean().loc[columns].pyroplot.scatter(
        ax=a,
        edgecolors="red",
        linewidths=2,
        c="none",
        s=50,
        label="Arithmetic Mean",
        no_ticks=True,
    )

    logratiomean(df.loc[:, columns]).pyroplot.scatter(
        ax=a,
        edgecolors="k",
        linewidths=2,
        c="none",
        s=50,
        label="Geometric Mean",
        axlabels=True,
        no_ticks=True,
    )
    a.legend(frameon=False, facecolor=None, loc=(0.8, 0.5))


import numpy as np

def prob_not(n:int = 20, days:int = 365):

    """
    probability of the birth dates not coinciding
    n : number of students in class
    days : days in the year (default 365)
    formulation:
        d! / ((d**n) * (365 - n)!)

    source : https://en.wikipedia.org/wiki/Birthday_problem

    """

    prob_no = np.math.factorial(days) / ((days**n) * np.math.factorial(days - n))

    return print("Probability of not coinciding: ", "{:.2%}".format(prob_no), "\n", "Probability of coinciding: ", "{:.2%}".format(1 - prob_no), "\n")

prob_not(20, 365)


import numpy as np

def loss(weights, cov, means, q):
    '''
    Loss function for markowitz mean-variance optimization. q \in [0, \infty), represents risk tolerance.
    '''
    term1 = np.matmul( weights.T , np.matmul(cov, weights) )
    term2 = - q*np.matmul( means.T, weights )
    return term1 + term2


import numpy as np
import pandas as pd
import scipy.spatial.distance as dt
import cvxopt as cvx
import matplotlib.pyplot as plt

images = np.genfromtxt("hw06_images.csv", delimiter=",")
labels = np.genfromtxt("hw06_labels.csv", delimiter=",")

# Divide the data, 1000 to training, 4000 to test
X_train = images[:1000]
y_train = labels[:1000].astype(int)
X_test = images[-4000:]
y_test = labels[-4000:].astype(int)
K = 5
# get number of samples and number of features
N_train = len(y_train)
N_test = len(y_test)

# define Gaussian kernel function
def gaussian_kernel(X1, X2, s):
    D = dt.cdist(X1, X2)
    K = np.exp(-D ** 2 / (2 * s ** 2))
    return (K)

def func(y_indexes, X_given, N_given, C, s, epsilon):
    K_trick = gaussian_kernel(X_given, X_given, s)
    yyK = np.matmul(y_indexes[:, None], y_indexes[None, :]) * K_trick
    # print(yyK)
    P = cvx.matrix(yyK)
    q = cvx.matrix(-np.ones((N_given, 1)))
    G = cvx.matrix(np.vstack((-np.eye(N_given), np.eye(N_given))))
    h = cvx.matrix(np.vstack((np.zeros((N_given, 1)), C * np.ones((N_given, 1)))))
    A = cvx.matrix(1.0 * y_indexes[None, :])
    b = cvx.matrix(0.0)

    # use cvxopt library to solve QP problems
    result = cvx.solvers.qp(P, q, G, h, A, b)
    alpha = np.reshape(result["x"], N_given)
    # Most of the alpha values should be 0.
    alpha[alpha < C * epsilon] = 0
    alpha[alpha > C * (1 - epsilon)] = C
    # print(alpha)
    # What is the purpose of cutting small values to 0?
    # What is the purpose of cutting larger values that are close to C directly to C value 10.
    # To find support indices!
    # Non zero alpha coefficient vectors are called support vectors!

    # find bias parameter
    support_indices, = np.where(alpha != 0)
    # print(support_indices)
    active_indices, = np.where(np.logical_and(alpha != 0, alpha < C))
    # Alpha points between 0 and C. As seen in the ipynb subject to.
    w0 = np.mean(
        y_indexes[active_indices] * (
                1 - np.matmul(yyK[np.ix_(active_indices, support_indices)], alpha[support_indices])))
    return alpha, w0

def predict(K_train, K_test, C, s):
    f_predicted_train = []
    f_predicted_test = []
    for k in range(1, K + 1):
        y_indexes = (y_train == k) * 2 - 1
        alpha, w0 = func(y_indexes, X_train, N_train, C, s, epsilon)
        # Train
        f_predicted_train.append(np.matmul(K_train, y_indexes[:, None] * alpha[:, None]) + w0)
        # Test
        f_predicted_test.append(np.matmul(K_test, y_indexes[:, None] * alpha[:, None]) + w0)
    y_predicted_test1 = np.argmax(f_predicted_test, axis=0) + 1
    y_predicted_test1 = y_predicted_test1.reshape(-1)

    y_predicted_train1 = np.argmax(f_predicted_train, axis=0) + 1
    y_predicted_train1 = y_predicted_train1.reshape(-1)

    return y_predicted_train1, y_predicted_test1

C = 10
epsilon = 1e-3
s = 10
K_train = gaussian_kernel(X_train, X_train, s)
K_test = gaussian_kernel(X_test, X_train, s)

y_predicted_train1, y_predicted_test1 = predict(K_train, K_test, C, s)

confusion_matrix = pd.crosstab(y_predicted_train1, y_train, rownames=['y_predicted'], colnames=['y_train'])
print("Confusion_matrix for train:")
print(confusion_matrix)

confusion_matrix = pd.crosstab(y_predicted_test1, y_test, rownames=['y_predicted'], colnames=['y_test'])
print("Confusion_matrix for test:")
print(confusion_matrix)

# Accuracy score
def accuracy_score(y_pred, y_truth):
    score = 0
    for index in range(len(y_truth)):
        if y_pred[index] == y_truth[index]:
            score += 1
    accuracy_score = float(score / len(y_truth))
    return accuracy_score

# Visualization
C_values = [0.1, 1, 10, 100, 1000]
s = 10
train_list = []
test_list = []
for c in C_values:
    # Train
    y_predicted_train, y_predicted_test = predict(K_train, K_test, c, s)

    accuracy_score_train = accuracy_score(y_predicted_train, y_train)
    train_list.append(accuracy_score_train)

    # Test
    accuracy_score_test = accuracy_score(y_predicted_test, y_test)
    test_list.append(accuracy_score_test)

str_list = ["10^-1", "10^0", "10^1", "10^2", "10^3"]
plt.figure(figsize=(10, 6))
plt.plot(str_list, train_list, "-ob", markersize=4, label='training')
plt.plot(str_list, test_list, "-or", markersize=4, label='test')
plt.xlabel("Regularization Parameter (C)")
plt.ylabel("Accuracy")
plt.legend(loc='upper left')
plt.show()


import cfwell.representations as rp
import cfwell.hamiltonian as ht
import cfwell.dynamics as dn

from math import pi, sqrt
from numpy import linspace

M,P = 4,11 # Initial state populations
ket0 = [M,P,0,0] # Initial state representation
natom = M+P # total number of atoms

U,J = 105, 72 # integr. hamiltonian parameters
nu,mu = 0,0 # breaking-of-int. parameters

tm = 2*pi*U/(J*J) * ((M-P)**2 - 1)

t0, tf = 0, 2*tm # initial time / final time
nt = 300 # number of time steps

time = linspace(t0, tf, num=nt)

params = {'interaction': U,
          'hopping': J,
          'break13': nu,
          'break24': mu}

Hint, Htun, Hb13, Hb24, H = ht.make_h(params, natom) # represents the hamilt.

# %% Quantum dynamics

Ni = rp.make_Ni(natom) # represents the Ni operators as matrices

psi0 = rp.representation(ket0) # represents the initial state in a vector

psit = dn.time_evol(psi0, H, time) # time-evolves the init. state
                                   # w/ the hamilt. provided

n1t, n2t, n3t, n4t = dn.quantum_dyn(Ni,            # calculates <n_i(t)> with
                                    time,          # the time-evolved states
                                    psit,          # provided.
                                    natom,
                                    square=False,
                                    plot=True)

n12t, n22t, n32t, n42t = dn.quantum_dyn(Ni,
                                        time,
                                        psit,
                                        natom,
                                        square=True, # enables Ni@Ni, which
                                        plot=True)   # results in:
                                                     # <n_i²(t)>

# %% NOON state fidelity with respect to time

from numpy import array, dot, conj

coefs = 1./2. * array([1, 1, 1, -1]) # superposition coefficients
kets = array([[M,P,0,0], [M,0,0,P], [0,P,M,0], [0,0,M,P]]) # states in superp.

# WARNING: each coefficient is related to each of the "kets" in the same order!

NOON = rp.make_state(coefs, kets)

fids = abs(array([dot(conj(NOON), psit[i,:]) for i in range(len(time))]))

#%% Energy bands

H_struc = [Hint, Htun, Hb13, Hb24] # hamiltonian matrix structures
ujlist, bvals = ht.bands(H_struc,
                         params,
                         natom,
                         plot=True) # energy bands calculation


from sympy import ( symbols, solve, diff, integrate, exp, sqrt, lambdify, pprint, Integral )

# The rate at which a substance grows is given by

x = symbols( 'x' )
R = 130*exp( 0.4 * x )

a, b = 0, 1.5
growth = integrate( R, ( x, a, b ) )
round( growth, 1 )


# Copyright 2021 BlackRock, Inc.
# 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
#     http://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.
from __future__ import annotations

from typing import Iterable, Union, cast

import numpy as np

def ackley(x: Union[float, Iterable[float]], a: float = 20, b: float = 0.2, c: float = 2 * np.pi) -> float:
    """https://www.sfu.ca/~ssurjano/ackley.html."""
    if isinstance(x, Iterable):
        x = np.array(x)  # This may consume the iterable (if it's a generator for example)
        n_dimensions = x.size
        if len(x.shape) != 1:
            raise AssertionError("Invalid shape")
    else:
        n_dimensions = 1
    term_1 = a * np.exp(-b * np.sqrt((1 / n_dimensions) * np.sum(x * x)))
    term_2 = np.exp((1 / n_dimensions) * np.sum(np.cos(c * x)))
    res = -term_1 - term_2 + a + np.exp(1)
    return cast(float, res)


import numpy as np
import matplotlib.pyplot as plt
import sys
x=[0.1,0.2,0.3,0.4,0.5,0.6,0.7]
y=[0.2,0.43,0.61,0.756,0.8,0.84,0.95]

x_mean=np.mean(x)
y_mean=np.mean(y)
print(x_mean,y_mean)

least_square=sum((x-x_mean)**2)
numerator=sum((x-x_mean)*(y-y_mean))
print(least_square,numerator)

slope=numerator/least_square
intercept=(y_mean-(slope*x_mean))

plt.scatter(x,y,c='red')
yt=[]
for i in range(0,len(x)):
    yt.append(slope*x[i]+intercept)

r_square=sum((yt-y_mean)**2)/sum((y-y_mean)**2)
std_error=np.sqrt((sum(yt-y_mean)**2)/(len(x)-2))
print(yt)
print('R_Square: ',r_square)
print('Std_Error: ',std_error)
s=0
for i in range(0,len(x)):
    s=s+((y[i]-yt[i])**2)
print('Squared_Error: ',s)
plt.plot(x,yt)
plt.show()


from numpy import std, mean, sqrt
import itertools as it

from bisect import bisect_left
from typing import List

import numpy as np
import pandas as pd
import scipy.stats as ss

from scipy.stats import ranksums

#correct if the population S.D. is expected to be equal for the two groups.
def cohen_d(x,y):
    nx = len(x)
    ny = len(y)
    dof = nx + ny - 2
    return (mean(x) - mean(y)) / sqrt(((nx-1)*std(x, ddof=1) ** 2 + (ny-1)*std(y, ddof=1) ** 2) / dof)

def VD_A(treatment: List[float], control: List[float]):
    """
    Computes Vargha and Delaney A index
    A. Vargha and H. D. Delaney.
    A critique and improvement of the CL common language
    effect size statistics of McGraw and Wong.
    Journal of Educational and Behavioral Statistics, 25(2):101-132, 2000
    The formula to compute A has been transformed to minimize accuracy errors
    See: http://mtorchiano.wordpress.com/2014/05/19/effect-size-of-r-precision/
    :param treatment: a numeric list
    :param control: another numeric list
    :returns the value estimate and the magnitude
    """
    m = len(treatment)
    n = len(control)

    if m != n:
        raise ValueError("Data d and f must have the same length")

    r = ss.rankdata(treatment + control)
    r1 = sum(r[0:m])

    # Compute the measure
    # A = (r1/m - (m+1)/2)/n # formula (14) in Vargha and Delaney, 2000
    A = (2 * r1 - m * (m + 1)) / (2 * n * m)  # equivalent formula to avoid accuracy errors

    levels = [0.147, 0.33, 0.474]  # effect sizes from Hess and Kromrey, 2004
    magnitude = ["negligible", "small", "medium", "large"]
    scaled_A = (A - 0.5) * 2

    magnitude = magnitude[bisect_left(levels, abs(scaled_A))]
    estimate = A

    return estimate, magnitude

if __name__ == '__main__':
    # town03 3.75
    x3 = [156,138,148,167,154,173]
    y3 = [111,110,96,123,107,125]
    # town05 3.67
    x5 = [555,557,546,559,551,549]
    y5 = [517,506,479,523,521,506]
    # town01 10.52
    x1 = [669,675,673,676,679,681]
    y1 = [555,546,511,527,542,510]
    # town07 7.08
    x7 = [409,390,386,368,424,391]
    y7 = [286,281,284,293,292,263]
    # borregas
    xb = [68, 79, 65, 74, 86, 85]
    yb = [49, 44, 51, 47, 65, 48]

    x = xb
    y = yb
    print(x, y)
    print ("cohen d = ", cohen_d(x, y))
    print('Wilcoxon rank-sum statistics p value', ranksums(x, y))
    print('Vargha-Delaney effect size', VD_A(x, y))

import numpy,from numpy,import scipy,from scipy


"""

"""
from numpy.linalg import pinv
from pandas import DataFrame, Series

def normal_equation(
    features: DataFrame, labels: Series, add_theta_zero: bool = False
) -> Series:
    if add_theta_zero:
        features.insert(0, "x0", [1 for _ in range(features.shape[0])])
    return Series(
        pinv(features.transpose().dot(features)).dot(features.transpose()).dot(labels)
    )


import numpy as np

def symmetric_gauss(A):
    if(A.size == 1):
        return np.array([1]), A.copy()

    U = np.eye(A.shape[0])
    U[0, 1:] = -A[0, 1:]/A[0,0]
    U_next = np.eye(A.shape[0])
    A_new = np.matmul(U.T, np.matmul(A, U))
    U_next[1:, 1:], A_new[1:, 1:] = symmetric_gauss(A_new[1:, 1:])
    return np.matmul(U, U_next), A_new.diagonal()

def cholesky(A):
    U, D = symmetric_gauss(A)
    E = np.diag(np.sqrt(D))
    return solve_triangular(U.T, E, lower=True).T

def factor_QR(A):
    if A.size == 1:                     #Base case
        return A/np.linalg.norm(A), np.linalg.norm(A)

    if A.size == 0:
        return np.eye(A.shape[0]), A

    x_bar = np.zeros((A.shape[0]))
    x_bar[0] = np.linalg.norm(A[:,0])

    u = A[:,0]-x_bar

    u = u/np.linalg.norm(u)

    mirror = (np.eye(A.shape[0])-2*np.outer(u, u))

    B = np.matmul(mirror, A)

    Q_next = np.eye((A.shape[0]))
    Q_next[1:, 1:], B[1:, 1:] = factor_QR(B[1:, 1:])

    return np.matmul(mirror.T, Q_next), B

def rotation_QR(A):
    if A.size == 1:                     #Base case
        return A/np.linalg.norm(A), np.linalg.norm(A)

    if A.size == 0:
        return np.eye(A.shape[0]), A

    B = A.copy()

    Q = np.eye(B.shape[0])

    for i in range(1, B.shape[0]):
        if B[i, 0]!=0:
            r = np.sqrt(B[0, 0]**2 + B[i, 0]**2)
            cosine_phi = B[0, 0]/r
            sine_phi = B[i,0]/r

            rot = np.eye(B.shape[0])        #Build the rotation matrix
            rot[0, 0] = cosine_phi
            rot[i, 0] = -sine_phi
            rot[0, i] = sine_phi
            rot[i, i] = cosine_phi

            B = np.matmul(rot, B)
            Q = np.matmul(Q, rot.T)

    Q_next = np.eye(B.shape[0])
    Q_next[1:, 1:], B[1:, 1:] = rotation_QR(B[1:, 1:])    #Recurse

    return np.matmul(Q, Q_next), B

def gram_schmidt(A):
    Q = np.zeros((A.shape[0], A.shape[0]), np.complex_)
    R = np.zeros_like(A, np.complex_)
    Q[:,0] = A[:, 0]
    norm = np.linalg.norm(Q[:, 0])
    Q[:, 0] = Q[:, 0]/norm
    R[0,0] = norm

    for i in range(1, A.shape[1]):
        coefficients = np.zeros((i), np.complex_)
        for k in range(i):
            coefficients[k] = np.dot(np.conj(A[:, i]), Q[:, k])

        Q[:, i] = A[:, i]-np.matmul(Q[:, :i], coefficients)
        norm = np.linalg.norm(Q[:, i])
        Q[:, i] = Q[:, i]/norm
        R[:i, i] = coefficients
        R[i,i] = norm

    return Q, R

def modified_gram_schmidt(A):
    R = np.zeros_like(A, np.complex_)
    Q = np.zeros((A.shape[0], A.shape[0]), np.complex_)

    V = A.astype(np.complex_)

    for i in range(A.shape[1]):
        R[i,i] = np.linalg.norm(V[:, i])
        q = V[:, i]/R[i,i]
        Q[:, i] = q
        for k in range(i, A.shape[1]):
            R[i, k] = np.dot(np.conj(q), V[:, k])
            V[:, k] = V[:, k]-R[i,k]*q

    return Q, R

if __name__=="__main__":
    Q, R = rotation_QR(np.array([[1.0,2.0, -4, 1], [3.0, 4.0,234.4, -3], [2,1,2,0.0004], [0,0,2,0]]))
    #Q, R = factor_QR(np.array([[1, -2], [1, 0], [1, 1], [1, 3]]).T)
    print(np.matmul(Q, Q.T))
    print()
    print(R)
    print()
    print(np.matmul(Q, R))


#coding=utf-8
import scipy.stats as stats
import numpy as np

def confidenceInterval(n_respondents, n_total, n_LikertLevels=3, significanceLevel=0.05, debug=False):
    """
    Description:
    --------------------
    This calculates the confidence interval for a Likert Response with k Likert scale levels.

    It uses the equation:

    Response% +/- ConfidenceInterval =

    (n_i + B/2) / (n + B) +/- sqrt( (B**2/4 + B * n_i * (1 - n_i/n)) / (n + B)**2 )

    where:

    n_i = number of respondents choosing the i-th level

    n = SUM(n_i, from:i=1, to:k), i.e. the total number of responses to the question

    k = maximum Likert scale levels (in our case, 3 after data is condensed)

    B = upper (alpha / k) 100th percentile of the chisquare distribution
        with 1 degree of freedom. (in this case 95%)

    EXAMPLE:
    --------
    Sample Question: "When doing a physics experiment, I don't think much
    about sources of systematic error. What do YOU think?"

    Data:
    -----
    Strongly Agree    : 95
    Agree             : 218
    Neutral           : 196
    Disagree          : 86
    Strongly Disagree : 27
    N/A               : 11

    for 6 scale likert scale (Strongly Agree to Strongly Disagree + N/A)
    95% Confidence Interval, alpha=0.05, k=6:

    Using R:
    > qchisq(0.05/6,1, lower.tail=FALSE)
    [1] 6.960401

    NOTE, in R:

    lower.tail logical; if TRUE (default), probabilities are P[X ≤ x],
    otherwise, P[X > x].

    But since this is Python:

    In [1]: import scipy.stats as stats

    In [2]: stats.chi2.ppf(1-0.05/6,1)
    Out[2]: 6.96040144105298

    NOTE, in Python:

    There is no 'lower.tail' to switch the range of the probability, thus
    we do '1-0.05/6'. (remember that 6 is just the number of Likert options
    in this example).

    Confidence Interval for percent saying "Strongly Agree":

    StronglyAgree% +/- CI = (95 + 6.96/2) / (633 + 6.96) +/- sqrt( (6.96**2/4 + 6.96 * 95 * (1 - 95/633)) / (633 + 6.96)**2 )
    StronglyAgree% +/- CI = 0.154 +/- 0.037

    Sources:
    [1] slide 13-14, http://faculty.nps.edu/rdfricke/OA4109/Lecture%209-1%20--%20Introduction%20to%20Survey%20Analysis.pdf
    [2] Accepted response, http://stackoverflow.com/questions/18070299/is-there-a-python-equivalent-of-rs-qchisq-function
    [3] scipy documentation, http://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.chi2.html
    [4] R documentation, https://stat.ethz.ch/R-manual/R-patched/library/stats/html/Chisquare.html

    Parameters:
    -------------------
    n_respondents : int (n_i)
    number of respondents with likert scale

    n_LikertLevels : int (k) : Default=3
    number of Likert Levels. Default is 3 due to combination of Likert
    responses (Strongly Dis/Agree and Dis/Agree)

    n_total : int (n)
    number of total responses to question

    significanceLevel : float (alpha = B * k) : Default=0.05
    The significance level typically known as 'alpha'. Default is 0.05.

    debug: Boolean : Default=False
    If True, returns B value.

    Returns:
    -------------------
    B : float : Default does not return
    The value returned from scipy.stats.chi2.ppf

    ResponsePercent : float
    Value responding with selected response.

    ConfidenceInterval : float
    The upper and lower bound of the interval.
    """
    if isinstance(n_respondents, int)==False:
        raise ValueError('n_respondents needs to be an integer.')

    if isinstance(n_LikertLevels, int)==False:
        raise ValueError('n_LikertLevels needs to be an integer.')

    if isinstance(n_total, int)==False:
        raise ValueError('n_total needs to be an integer.')

    if isinstance(significanceLevel, float)==False:
        raise ValueError('significanceLevel needs to be a float.')

    if significanceLevel > 1 or significanceLevel < 0:
        raise ValueError('significanceLevel needs to be between 0 and 1.')

    if n_respondents <= 0:
        raise ValueError('n_respondents needs to be greater than 0.')

    if n_LikertLevels <= 0:
        raise ValueError('n_LikertLevels needs to be greater than 0.')

    if n_total <= 0:
        raise ValueError('n_total needs to be greater than 0.')

    B = stats.chi2.ppf(1-significanceLevel/n_LikertLevels, 1)

    ResponsePercent = (n_respondents + B/2) / (n_total + B)
    ConfidenceInterval = np.sqrt(((B**2)/4 + B * n_respondents * (1 - n_respondents/n_total))/(n_total + B)**2)

    if debug==True:
        return B, ResponsePercent, ConfidenceInterval
    else:
        return ResponsePercent, ConfidenceInterval


import numpy as np

def Lanczos(t0,t,fc,ftype='high'):
	'''
	Calculate the Lanczos-squared function centred upon t0 for an
	arbitrary set of points in time.

	'''

	#find the points within 1.5*(1.0/fc) of t0 to apply the function
	#the rest will be set to zero
	n = t.size
	filt = np.zeros(n,dtype='float32')
	pc = 1.0/fc
	use = np.where((t > t0-1.5*pc) & (t < t0+1.5*pc))[0]

	#get dt
	dt = (t[use] - t0)/(1.5*pc)

	#calculate the function
	pidt2 = np.pi*dt
	filt[use] = (np.sin(pidt2)/pidt2)**2
	filt[use] = filt[use]*np.sin(pidt2*3)/(pidt2*3)

	#check if any of the dt's are 0
	dt0 = np.where(pidt2 == 0)[0]
	if dt0.size > 0:
		filt[use[dt0]] = 1.0

	#calculate the normalization factor (integrate)
	#tr = t[use[-1]] - t[use[0]]
	norm = np.trapz(filt[use])
	#norm = np.trapz(filt[use],(use.size-1)*(t[use]-t[use[0]])/tr)
	if ftype == 'low':
		filt = filt/norm
	else:
		ic = np.where(t == t0)[0]
		filt = -filt/norm
		filt[ic] = 1.0

	return filt


# project euler https://projecteuler.net/

# 1. Find the sum of all positive integers less than 1000 that are multiples of 3 or 5
def sum_mulitples(x, y, n):
    """find the sum of all multiples of x and y less than n"""
    return sum([(num % x == 0 or num % y == 0)*num for num in range(1, n)])

print('sum_mulitples', sum_mulitples(3, 5, 1000))

# 2. Find the sum of the even-valued terms in the Fibonacci sequence whose values do not exceed four million.
def sum_even_fibonacci(n):
    """sum of the even-valued terms of the fibonacci sequence not exceeding n"""
    result = 0
    if n >= 2:
        x, y = 1, 1
        for _ in range(n):
            x, y = y, x + y
            if x > n:
                break
            if x % 2 == 0:
                # print(x, y)
                result += x
    return result

print('sum_even_fibonacci', sum_even_fibonacci(4000000))

# 3. What is the largest prime factor of the number 600851475143?
def largest_prime_factor(n):
    """find the largest prime factor of n"""
    # NOTE 600851475143 is way too big to find every possible factor quickly.
    # we must divide n by every "prime" starting from 2.
    current = 2
    max_factor = n
    while current < max_factor:
        while max_factor % current == 0:
            max_factor = max_factor//current
            # print(current, max_factor)
        if current % 2 == 0:
            current += 1
        else:
            current += 2
    return max_factor

print('largest_prime_factor', largest_prime_factor(600851475143))

# 4. What is the largest palindromic number that is the product of two 3 digit numbers?
def largest_palindrome_number(lo, hi):
    """largest palindromic product within a given range (using strings)"""
    result = 0
    for i in reversed(range(lo, hi)):
        for j in reversed(range(lo, hi)):
            val = list(str(i*j))
            # print(i, j, val)
            is_palindrome = True
            for k in range(len(val)//2 + 1):
                # print(i, j, k, val, val[k], val[len(val)-1-k])
                if val[k] != val[len(val)-1-k]:
                    is_palindrome = False
                    break
            if is_palindrome:
                # print(i, j, val)
                if i*j > result:
                    result = i*j
    return result
# 111**2 = 12321

print('largest_palindrome_number', largest_palindrome_number(100, 1000))

# 5. What is the smallest positive number that is evenly divisible by all of the numbers from 1 to 20?
def find_sequence_scm(n: int) -> int:
    """finds the smallest number that divides everything from 1 to n"""
    factorial = 1
    for i in range(2, n+1):
        factorial *= i
    # this number is a multiple of the LCM.
    for i in range(n, factorial, n):
        if (factorial % i == 0):
            for factor in range(2, n+1):
                if i % factor != 0:
                    break  # not the scm
                if factor == n:
                    return i
    return factorial  # default

print('find_sequence_scm', find_sequence_scm(20))

# 6. Find the difference between the sum of the squares and the square of the sum of the first 100 natural numbers.
def diff_sum_squares(n: int) -> int:
    """difference between the sum of the squares and the square of the sum of the first n naturals"""
    return sum([i for i in range(1, n+1)])**2 - sum([i**2 for i in range(1, n+1)])

print('diff_sum_squares', diff_sum_squares(100))

# 7. What is the 10001st prime number?
def find_nth_prime(n: int) -> int:
    """modified sieve checks every natural number against all previous primes to find the nth prime"""
    assert n >= 2
    current = 3
    primes = [current]
    if n == 1:
        return current
    # add 1 continuously in a loop checking if the number divides any of the previously found primes
    while len(primes) < n:
        if current == 2:
            current += 1
        else:
            current += 2
        is_prime = True
        for prime in primes:
            if current % prime == 0:
                is_prime = False
                break  # not a prime
        if is_prime:
            primes.append(current)
            # print(primes)
    return primes[-1]

print('find_nth_prime', find_nth_prime(10001))

# 8. Find thirteen adjacent digits in the 1000-digit number that have the greatest product
def greatest_sequence_product(val: int, n: int) -> (int, list):
    """given an integer find the subsequence of a given length having the greatest product"""
    digits = list(map(int, str(val)))
    max_product = 0
    for index in range(len(digits)):
        sequence = digits[index:index+n]
        if len(sequence) < n:
            break
        product = digits[index]
        for s in sequence[1:n]:
            product *= s
        if max_product < product:
            max_product = product
        # print(index, sequence, product, max_product)
    return max_product, sequence

# val = 1234567890
val = 7316717653133062491922511967442657474235534919493496983520312774506326239578318016984801869478851843858615607891129494954595017379583319528532088055111254069874715852386305071569329096329522744304355766896648950445244523161731856403098711121722383113622298934233803081353362766142828064444866452387493035890729629049156044077239071381051585930796086670172427121883998797908792274921901699720888093776657273330010533678812202354218097512545405947522435258490771167055601360483958644670632441572215539753697817977846174064955149290862569321978468622482839722413756570560574902614079729686524145351004748216637048440319989000889524345065854122758866688116427171479924442928230863465674813919123162824586178664583591245665294765456828489128831426076900422421902267105562632111110937054421750694165896040807198403850962455444362981230987879927244284909188845801561660979191338754992005240636899125607176060588611646710940507754100225698315520005593572972571636269561882670428252483600823257530420752963450
print('greatest_sequence_product', greatest_sequence_product(val, 13))  # 13

# 9. Find the Pythagorean triplet for which a + b + c = 1000 and return its product.
def pythagorean_triplet(n: int) -> ((int, int, int), int):
    """find the Pythagorean triplet whose sum equal n and return its product. """
    import math
    for c in reversed(range(int(math.sqrt(n)), n//2)):
        for b in reversed(range(1, c)):
            a = n - c - b
            if a > c:
                break
            # print("c", c, "b", b, "a", a)
            if (c**2 == a**2 + b**2):
                return (a, b, c), a*b*c

# [((200, 375, 425), 31875000)]
print('pythagorean_triplet', pythagorean_triplet(1000))

# 10. Find the sum of the primes less than two million.

def sum_of_primes(n: int) -> int:
    """return the sum of all primes less than n"""
    # just find every prime using erastosthenes, and tally as you find them
    return sum(find_primes(n))

def find_primes(max_value):
    """returns every prime up to a given value."""

    # todo use tuple instead of dict, almost as fast
    numbers = {a: True for a in range(2, max_value+1)}
    sieve = [n for n in numbers if n <= max_value**0.5]
    for current in sieve:
        if numbers[current]:
            prime_multiple = current**2
            while prime_multiple <= max_value:
                if numbers[prime_multiple]:
                    numbers[prime_multiple] = False
                prime_multiple += current
    primes = [p for p in numbers if numbers[p]]
    return primes

print('sum_of_primes', sum_of_primes(2000000))

# 11. Find the maximum product of four adjacent numbers in a 20x20 matrix.

def matrix_search(my_list: list) -> int:
    import numpy as np
    arr = np.array(my_list)
    (x, y) = arr.shape
    # print(arr)
    return crawl_matrix(arr, x)

def crawl_matrix(arr, n):
    import numpy as np
    max_product = 0
    for x in range(n):
        for y in range(n):
            perform_vertical_check = False
            if (x+3 <= n-1):
                perform_vertical_check = True
                max_product = check_vertical(arr, x, y, max_product)
                if (y-3 >= 0):
                    max_product = check_backward_diag(arr, x, y, max_product)
            if (y+3 <= n-1):
                max_product = check_horizontal(arr, x, y, max_product)
                if (perform_vertical_check):
                    max_product = check_forward_diag(arr, x, y, max_product)
    return max_product

def check_vertical(arr, x, y, max_product):
    product = arr[x,y]
    for i in range(x+1, x+4):
        product *= arr[i, y]
        if (product > max_product):
            max_product = product
            # print("vertical", x, y, arr[x,y], "max_product", max_product)
    return max_product

def check_backward_diag(arr, x, y, max_product):
    product = arr[x, y]
    j = y
    for i in range(x+1, x+4):
        j -= 1
        product *= arr[i, j]
        if (product > max_product):
            max_product = product
            # print("back diag", x, y, arr[x,y], "max_product", max_product)
    return max_product

def check_horizontal(arr, x, y, max_product):
    product = arr[x, y]
    for j in range(y+1, y+4):
        product *= arr[x, j]
        if (product > max_product):
            max_product = product
            # print("horizontal", x, y, arr[x,y], "max_product", max_product)
    return max_product

def check_forward_diag(arr, x, y, max_product):
    product = arr[x, y]
    j = y
    for i in range(x+1, x+4):
        j += 1
        product *= arr[i, j]
        if (product > max_product):
            max_product = product
            # print("fwd diag", x, y, arr[x,y], "max_product", max_product)
    return max_product

my_matrix = [
    [8,  2, 22, 97, 38, 15,  0, 40,  0, 75,  4,  5,  7, 78, 52, 12, 50, 77, 91,  8],
    [49, 49, 99, 40, 17, 81, 18, 57, 60, 87, 17, 40, 98, 43, 69, 48,  4, 56, 62,  0],
    [81, 49, 31, 73, 55, 79, 14, 29, 93, 71, 40, 67, 53, 88, 30,  3, 49, 13, 36, 65],
    [52, 70, 95, 23,  4, 60, 11, 42, 69, 24, 68, 56,  1, 32, 56, 71, 37,  2, 36, 91],
    [22, 31, 16, 71, 51, 67, 63, 89, 41, 92, 36, 54, 22, 40, 40, 28, 66, 33, 13, 80],
    [24, 47, 32, 60, 99,  3, 45,  2, 44, 75, 33, 53, 78, 36, 84, 20, 35, 17, 12, 50],
    [32, 98, 81, 28, 64, 23, 67, 10, 26, 38, 40, 67, 59, 54, 70, 66, 18, 38, 64, 70],
    [67, 26, 20, 68,  2, 62, 12, 20, 95, 63, 94, 39, 63,  8, 40, 91, 66, 49, 94, 21],
    [24, 55, 58,  5, 66, 73, 99, 26, 97, 17, 78, 78, 96, 83, 14, 88, 34, 89, 63, 72],
    [21, 36, 23,  9, 75,  0, 76, 44, 20, 45, 35, 14,  0, 61, 33, 97, 34, 31, 33, 95],
    [78, 17, 53, 28, 22, 75, 31, 67, 15, 94,  3, 80,  4, 62, 16, 14,  9, 53, 56, 92],
    [16, 39,  5, 42, 96, 35, 31, 47, 55, 58, 88, 24,  0, 17, 54, 24, 36, 29, 85, 57],
    [86, 56,  0, 48, 35, 71, 89,  7,  5, 44, 44, 37, 44, 60, 21, 58, 51, 54, 17, 58],
    [19, 80, 81, 68,  5, 94, 47, 69, 28, 73, 92, 13, 86, 52, 17, 77,  4, 89, 55, 40],
    [4,  52,  8, 83, 97, 35, 99, 16,  7, 97, 57, 32, 16, 26, 26, 79, 33, 27, 98, 66],
    [88, 36, 68, 87, 57, 62, 20, 72,  3, 46, 33, 67, 46, 55, 12, 32, 63, 93, 53, 69],
    [4,  42, 16, 73, 38, 25, 39, 11, 24, 94, 72, 18,  8, 46, 29, 32, 40, 62, 76, 36],
    [20, 69, 36, 41, 72, 30, 23, 88, 34, 62, 99, 69, 82, 67, 59, 85, 74,  4, 36, 16],
    [20, 73, 35, 29, 78, 31, 90,  1, 74, 31, 49, 71, 48, 86, 81, 16, 23, 57,  5, 54],
    [1,  70, 54, 71, 83, 51, 54, 69, 16, 92, 33, 48, 61, 43, 52,  1, 89, 19, 67, 48]
]

print("matrix max product", matrix_search(my_matrix))

# 12 What is the value of the first triangle number to have over five hundred divisors?
def triangle_number_divisors(n) -> int:
    '''
    returns the first triangle number with at least n divisors
    '''
    # outer loop adds up the natural numbers and keeps sum
    t = 0  # the natural numbers
    s = t  # the sum of t
    d = 0  # the count of divisors of s
    while n >= d:
        t += 1
        s += t
        # inner loop finds divisors of each and breaks when it finds one with > n divisors
        d = 0
        for i in range(1, s//2):
            if i > s//i:
                break
            if s % i == 0:
                d += 2
    return(s)

print("triangle_number_divisors ", triangle_number_divisors(500))


import numpy as np
from matplotlib import pyplot as plt

def least_norm(A, closures, pinv=False):
    '''
        Solve: Ax = b
        Find the minimum norm vector 'x' of phases that can explain 'b' phase closures
    '''
    if pinv:
        return np.linalg.pinv(A) @ closures
    return A.T @ np.linalg.inv(A @ A.T) @ closures

def get_closures(A, phi):
    '''
        Use matrix mult to generate vector of phase closures such that
        the angle xi = phi_12 + phi_23 - phi_13
    '''
    return A @ np.angle(phi)

# Vector of phases with zero phase closure errors
closed = np.exp(1j * np.array([0.5, 1, 0.5]))

# Vector of phases with non-zero closure errors
unclosed = np.exp(1j * np.array([0.5, 1 + 0.1, 0.5 + 0.1]))

def main(phis):
    ind = np.array([1, 2, 3])
    A = np.array([[1, -1, 1]])

    # Phases to phase closures
    closures = get_closures(A, phis)

    # Phase closures back to phases
    least_norm_phi = least_norm(A, closures)
    print('Phase closures: ', closures)

    plt.plot(ind, np.angle(closed), label='observed phases')
    plt.plot(ind, np.angle(least_norm_phi), label='least norm phases ')
    plt.plot(ind, np.angle(closed * least_norm_phi.conj()),
             label='Corrected Phases')

    plt.legend(loc='lower left')
    plt.show()

main(unclosed)


import numpy as np
from scipy.optimize import curve_fit

def gauss(x,a,x0,sigma):
    return a*np.exp(-(x-x0)**2/(2*sigma**2))

def gaussian_fit(x, y, x_smooth=None, n_pts=200):
    """
    Fits a Gaussian to some data - x and y. Returns predicted interpolation values.

    Parameters
    ----------
    x: list-like
        The x values of the data to fit to.
    y: list-like
        The y values of the data to fit to.
    x_smooth: list-like
        The exact x values to interpolate for. Supercedes `n_pts`.
    n_pts: int
        The number of evenly spaced points spanning the range of `x` to interpolate for.
    """
    if x_smooth is None:
        x_smooth = np.linspace(x.min(), x.max(), n_pts)
    mean, sigma = np.nanmean(y), np.nanstd(y)
    popt,pcov = curve_fit(gauss,x,y,p0=[1,mean,sigma], maxfev=np.iinfo(np.int32).max)
    return gauss(x_smooth,*popt)

def poly_fit(x, y, degree, x_smooth=None, n_pts=200):
    """
    Fits a polynomial of any positive integer degree to some data - x and y. Returns predicted interpolation values.

    Parameters
    ----------
    x: list-like
        The x values of the data to fit to.
    y: list-like
        The y values of the data to fit to.
    x_smooth: list-like
        The exact x values to interpolate for. Supercedes `n_pts`.
    n_pts: int
        The number of evenly spaced points spanning the range of `x` to interpolate for.
    degree: int
        The degree of the polynomial to fit.
    """
    if x_smooth is None:
        x_smooth = np.linspace(x.min(), x.max(), n_pts)
    return np.array([np.array([coef*(x_val**current_degree) for coef, current_degree in
                               zip(np.polyfit(x, y, degree), range(degree, -1, -1))]).sum() for x_val in x_smooth])


import numpy as np
from abc import ABC

class BaseModel(ABC):
    """
    Abstract Model Class used as interface of all regression models.

    :param model: Stored model data (NumPy array in most cases)
    :type model: np.ndarray
    :param data: input data for training or testing
    :type data: np.ndarray
    :param labels: labels of the input data for training
    :type labels: np.ndarray
    """

    def __init__(self):
        self.model = None

    def fit(self, data: np.ndarray, labels: np.ndarray):
        """
        :param data: input data for training
        :type data: np.ndarray
        :param labels: labels of the input data for training
        :type labels: np.ndarray
        """

        pass

    def predict(self, data: np.ndarray):
        """
        :param data: input data for testing
        :type data: np.ndarray
        :return: predicted data
        :rtype: np.ndarray
        """

        pass

class ConstantModel(BaseModel):
    """
    Constant model class that uses the average of each feature as prediction.

    :param model: Stored model data (NumPy array in most cases)
    :type model: np.ndarray
    :param data: input data for training or testing
    :type data: np.ndarray
    :param labels: labels of the input data for training
    :type labels: np.ndarray
    """

    def fit(self, data: np.ndarray, labels: np.ndarray) -> None:
        """
        Method for fitting the training data and determining and storing the model parameters. In this case
        the model returns always the data average.

        :param data: input data for training
        :type data: np.ndarray
        :param labels: labels of the input data for training
        :type labels: np.ndarray
        :return: None
        """

        w = labels.mean()
        self.model = w

    def predict(self, data: np.ndarray) -> np.ndarray:
        """
        Given the input data it returns the predicted output for each input point using the model, in this case
        the average.

        :param data: input data for testing
        :type data: np.ndarray
        :return: predicted data
        :rtype: np.ndarray
        """

        return np.ones(len(data)) * self.model

class LinearRegression(BaseModel):
    """
    Linear regression model class that uses the following model for predictions:
    .. math::
        y = \\omega*X
    """

    def fit(self, data: np.ndarray, labels: np.ndarray) -> None:
        """
        Method for fitting the training data and determining and storing the model parameters :math:`\\omega`.

        :param data: input data for training
        :type data: np.ndarray
        :param labels: labels of the input data for training
        :type labels: np.ndarray
        :return: None
        """

        if len(data.shape) == 1:
            w = data.T.dot(labels) / data.T.dot(data)
        else:
            w = np.linalg.inv(data.T.dot(data)).dot(data.T).dot(labels)
        self.model = w

    def predict(self, data: np.ndarray) -> np.ndarray:
        """
        Given the input data it returns the predicted output for each input point using the model, in this case:

        .. math::
            y = \\omega*X

        :param data: input data for testing
        :type data: np.ndarray
        :return: predicted data
        :rtype: np.ndarray
        """

        return self.model * data

class LinearRegressionWithB(BaseModel):
    """
    Affine regression model class that uses the following model for predictions:

    .. math::
        y = \\omega*X + b
    """

    def fit(self, data: np.ndarray, labels: np.ndarray) -> None:
        """
        Method for fitting the training data and determining and storing the model parameters :math:`\\omega` and b.

        :param data: input data for training
        :type data: np.ndarray
        :param labels: labels of the input data for training
        :type labels: np.ndarray
        :return: None
        """

        x_expanded = np.vstack((data, np.ones(len(data)))).T
        w = np.linalg.inv(x_expanded.T.dot(x_expanded)).dot(x_expanded.T).dot(labels)
        self.model = w

    def predict(self, data: np.ndarray) -> np.ndarray:
        """
        Given the input data it returns the predicted output for each input point using the model, in this case:

        .. math::
            y = \\omega*X + b

        :param data: input data for testing
        :type data: np.ndarray
        :return: predicted data
        :rtype: np.ndarray
        """

        x_expanded = np.vstack((data, np.ones(len(data)))).T
        return x_expanded.dot(self.model)