import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from Regression import Regression
from sklearn.model_selection import train_test_split

class L1_Regularizer(object):
    ''' L1 Regularizer for Lasso Regression '''

    def __init__(self, alpha: float) -> None:
        self.alpha = alpha

    def __call__(self, w):
        return self.alpha * np.sum(np.abs(w))

    def grad(self, w):
        return self.alpha * np.sign(w)

class LassoRegression(Regression):
    '''
    Lasso regression model (Inherited from Regression class)
    - L1 Regularizartion used
    '''

    def __init__(self, alpha: float) -> None:
        '''
        Initialize the model

        :param alpha: L1 regularization parameter
        '''

        super().__init__()
        self.regularization = L1_Regularizer(alpha)

    def fit(self, X: np.array, y: np.array, epochs: int = 100, learning_rate: int = 0.0001) -> None:
        '''
        Fit the model to the training data X using Gradient Descent and L1 regularization

        :param X: training data
        :param y: target data
        :param epochs: number of epochs (Iterations) in case gradient decent is used (Default: 100)
        :param learning_rate: learning rate for gradient decent (Deafult: 0.0001)
        :return: None
        '''

        self.epochs = epochs
        self.learning_rate = learning_rate

        # Initialize weight
        self.initialize_weight(n_features = len(X))

        n_features = len(X)

        for _ in range(self.epochs):

            # Make prediction
            y_pred = np.dot(X, self.weight) + self.bias

            # Calculate error
            error = y - y_pred

            # Calculate gradient descent
            D_m = -(2 / n_features) * sum(X * (error))     # Partial Derivative for M (Weights)
            D_c = -(2 / n_features) * sum(error)           # Partial Derivative for C (Bias)

            # Apply L1 regularization (Lasso)
            D_m += self.regularization.grad(self.weight)

            # Update weights and bias
            self.weight -= (learning_rate * D_m)
            self.bias -= (learning_rate * D_c)

if __name__ == '__main__':

    # Load the dataset
    df = pd.read_csv('data/scatter.csv')

    # Preparing the data
    X = np.array(df.iloc[:, 0])
    y = np.array(df.iloc[:, 1])
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3)

    # Define model
    regressor = LassoRegression(alpha = 0.01)
    regressor.fit(X_train, y_train, epochs = 1000, learning_rate = 0.0001)

    # Make predictions
    y_pred = regressor.predict(X_test)

    # Plotting the results
    plt.figure(figsize = (5, 3))
    plt.scatter(X_train, y_train , color = 'green')
    plt.scatter(X_test, y_test , color = 'blue')
    plt.plot(X_test , y_pred , color = 'r' , lw = 1)
    plt.show()


import numpy             as np
import matplotlib.pyplot as plt

from mpl_toolkits.mplot3d import Axes3D

from hilbert import decode

num_dims = 3

def draw_curve(ax, num_bits):

  # The maximum Hilbert integer.
  max_h = 2**(num_bits*num_dims)

  # Generate a sequence of Hilbert integers.
  hilberts = np.arange(max_h)

  # Compute the 2-dimensional locations.
  locs = decode(hilberts, num_dims, num_bits)

  # Draw
  ax.plot(locs[:,0], locs[:,1], locs[:,2], '.-')
  # ax.set_aspect('equal')
  ax.set_title('%d bits per dimension' % (num_bits))
  ax.set_xlabel('dim 1')
  ax.set_ylabel('dim 2')
  ax.set_zlabel('dim 3')

fig = plt.figure(figsize=(16,4))
for ii, num_bits in enumerate([1, 2, 3, 4]):
  ax = fig.add_subplot(1,4,ii+1, projection='3d')
  draw_curve(ax, num_bits)
plt.savefig('example_3d.png', bbox_inches='tight')


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

def euler_forward(f,t0,x0,b,h):
    n= int((b-t0)/h)
    t_arr=[t0]
    X_arr=[x0]
    for i in range(n):
        xn = x0 + h * f(t0,x0)
        x0 = xn
        t0 = t0+h
        X_arr.append(x0)
        t_arr.append(t0)
    return t_arr,X_arr

def RK2(f,t0,x0,b,h):
    n= int((b-t0)/h)
    t_arr=[t0];X_arr=[x0]
    for i in range(n):
        k1=h*f(t0,x0)
        k2=h*f(t0+h,x0+k1)
        k_RK2=(k1+k2)/2
        xn=x0+k_RK2
        x0 = xn
        t0 = t0+h
        X_arr.append(x0)
        t_arr.append(t0)
    return t_arr,X_arr

def graph(x1_1,y1_1,x1_2,y1_2,x1_3,y1_3,x1_lab,y1_lab,tit_1,x2_1,y2_1,x2_2,y2_2,x2_3,y2_3,x2_lab,y2_lab,tit_2,x3_1,y3_1,x3_2,y3_2,x3_3,y3_3,x3_lab,y3_lab,tit_3,x4,y4_1,y4_2,x4_lab,y4_lab,tit_4,x_an,y_an,tit):
    fig, axs = plt.subplots(2, 2)
    axs[0, 0].plot(x1_1, y1_1,marker="*",label="EULER")
    axs[0, 0].plot(x1_2, y1_2,marker="*",label="RK2")
    axs[0, 0].plot(x1_3, y1_3,marker="o",label="ANALYTIC")
    axs[0,0].set_xlabel(x1_lab)
    axs[0,0].set_ylabel(y1_lab)
    axs[0, 0].set_title(tit_1,c="magenta")
    axs[0, 1].plot(x_an, y_an,marker="|",label="ANALYTIC")
    axs[0, 1].plot(x2_1, y2_1,marker="*", label="h ={}".format(x2_1[1]-x2_1[0]))
    axs[0, 1].plot(x2_2, y2_2,marker="*", label="h ={}".format(x2_2[1]-x2_2[0]))
    axs[0, 1].plot(x2_3, y2_3,marker="*", label="h ={}".format(x2_3[1]-x2_3[0]))
    axs[0,1].set_xlabel(x2_lab)
    axs[0,1].set_ylabel(y2_lab)
    axs[0, 1].set_title(tit_2,c="magenta")
    axs[1,0].plot(x_an, y_an,marker="|",label="ANALYTIC")
    axs[1,0].plot(x3_1, y3_1,marker="*", label="h ={}".format(x3_1[1]-x3_1[0]))
    axs[1,0].plot(x3_2, y3_2,marker="*", label="h ={}".format(x3_2[1]-x3_2[0]))
    axs[1,0].plot(x3_3, y3_3,marker="*", label="h ={}".format(x3_3[1]-x3_3[0]))
    axs[1,0].set_xlabel(x3_lab)
    axs[1,0].set_ylabel(y3_lab)
    axs[1,0].set_title(tit_3,c="magenta")
    axs[1, 1].scatter(x4,y4_1 , label="EULER")
    axs[1, 1].scatter(x4,y4_2 , label="RK2")
    axs[1,1].set_xlabel(x4_lab)
    axs[1,1].set_ylabel(y4_lab)
    axs[1,1].set_title(tit_4,c="magenta")
    axs[0,0].grid()
    axs[1,0].grid()
    axs[0,1].grid()
    axs[1,1].grid()
    axs[0,0].legend()
    axs[1,0].legend()
    axs[0,1].legend()
    axs[1,1].legend()
    fig.suptitle(tit,c="red")
    plt.show()

def Anal(x0,t0,b,h,tau):
    t_arr=[]
    X_arr=[]
    e=np.arange(t0,b+h,h)
    for t in e:
        D=x0*np.exp(-1*t/tau)
        X_arr.append(D)
        t_arr.append(t)
    return t_arr,X_arr

def q3a(t0,x0,t_h):
    b=5*t_h
    tou=t_h/0.693
    f= lambda x,N : -1*N/tou
    a=euler_forward(f,t0,x0,b,t_h/8)
    a1=euler_forward(f,t0,x0,b,t_h/2)
    a2=euler_forward(f,t0,x0,b,t_h/4)
    b1=RK2(f,t0,x0,b,t_h/8)
    b2=RK2(f,t0,x0,b,t_h/2)
    b3=RK2(f,t0,x0,b,t_h/4)
    c=Anal(x0,t0,b,t_h/8,tou)
    eu=[]
    rk=[]
    for i in range(len(a[1])):
        eu.append((c[1][i]-a[1][i])/c[1][i])
        rk.append((c[1][i]-b1[1][i])/c[1][i])
    data = {"No. of half lives":np.array(a[0])/t_h,"N (Euler)":a[1],"N (RK2)":b1[1],"N (Analytic)":c[1],"Ab. Error in Euler":eu,"Ab. Error in RK2":rk}
    print(pd.DataFrame(data))
    graph(np.array(a[0])/t_h,a[1],np.array(b1[0])/t_h,b1[1],np.array(c[0])/t_h,c[1],"No. of half lives","No. of particles","Euler and RK2",a[0],a[1],a1[0],a1[1],a2[0],a2[1],"t","N","Euler method for various h",b1[0],b1[1],b2[0],b2[1],b3[0],b3[1],"t","N","RK2 method for various h",a[0],eu,rk,"Time(t)","Absolute Error","Error plot",c[0],c[1],"RADIOACTIVE DECAY")

def q3b(t0,x0,R,C):
    b=5*R*C
    tou=R*C
    f= lambda x,V : -1*V/tou
    a=euler_forward(f,t0,x0,b,tou/10)
    a1=euler_forward(f,t0,x0,b,tou/5)
    a2=euler_forward(f,t0,x0,b,tou/15)
    b1=RK2(f,t0,x0,b,tou/10)
    b2=RK2(f,t0,x0,b,tou/5)
    b3=RK2(f,t0,x0,b,tou/15)
    c=Anal(x0,t0,b,tou/10,tou)
    eu=[]
    rk=[]
    for i in range(len(a[1])):
        eu.append((c[1][i]-a[1][i])/c[1][i])
        rk.append((c[1][i]-b1[1][i])/c[1][i])
    data = {"t/RC":np.array(a[0])/tou,"V (Euler)":a[1],"V (RK2)":b1[1],"V (Analytic)":c[1],"Ab. Error in Euler":eu,"Ab. Error in RK2":rk}
    print(pd.DataFrame(data))
    graph(np.array(a[0])/tou,np.array(a[1])/a[1][0],np.array(b1[0])/tou,np.array(b1[1])/b1[1][0],np.array(c[0])/tou,np.array(c[1])/c[1][0],"t/RC","V/V0","Euler and RK2",a[0],a[1],a1[0],a1[1],a2[0],a2[1],"t","V","Euler method for various h",b1[0],b1[1],b2[0],b2[1],b3[0],b3[1],"t","V","RK2 method for various h",a[0],eu,rk,"Time(t)","Absolute Error","Error plot",c[0],c[1],"RC CIRCUIT")

def q3c(t0,x0,eta,rad,m):
    tou=m/(np.pi*6*rad*eta)
    b=4*tou
    f= lambda x,v : -1*v/tou
    a=euler_forward(f,t0,x0,b,tou/10)
    a1=euler_forward(f,t0,x0,b,tou/5)
    a2=euler_forward(f,t0,x0,b,tou/15)
    b1=RK2(f,t0,x0,b,tou/10)
    b2=RK2(f,t0,x0,b,tou/5)
    b3=RK2(f,t0,x0,b,tou/15)
    c=Anal(x0,t0,b,tou/10,tou)
    eu=[]
    rk=[]
    for i in range(len(a[1])):
        eu.append((c[1][i]-a[1][i])/c[1][i])
        rk.append((c[1][i]-b1[1][i])/c[1][i])
    data1 = {"t/m/(pi*6*rad*eta)":np.array(a[0])/tou,"v (Euler)":a[1],"v (RK2)":b1[1],"v (Analytic)":c[1],"Ab. Error in Euler":eu,"Ab. Error in RK2":rk}
    print(pd.DataFrame(data1))
    graph(np.array(a[0])/tou,np.array(a[1])/a[1][0],np.array(b1[0])/tou,np.array(b1[1])/b1[1][0],np.array(c[0])/tou,np.array(c[1])/c[1][0],"t/m/(pi*6*rad*eta)","v/v0","Euler and RK2",a[0],a[1],a1[0],a1[1],a2[0],a2[1],"t","v","Euler method for various h",b1[0],b1[1],b2[0],b2[1],b3[0],b3[1],"t","v","RK2 method for various h",a[0],eu,rk,"Time(t)","Absolute Error","Error plot",c[0],c[1],"STOKES LAW")

t0=0;x0=2e4;t_h=4
print("----------------------------Q. 3a (RADIOACTIVE DECAY) For h= ",t_h/8,"-----------------------------")
q3a(t0,x0,t_h)

t0=0;x0=10;R=1e3;C=1e-6
print("----------------------------Q. 3b (RC CIRCUIT) For h= ",R*C/10,"-----------------------------")
q3b(t0,x0,R,C)

t0=0;x0=10;eta=10;rad=0.2;m=200
print("----------------------------Q. 3c (STOKES LAW) For h= ",m/(np.pi*6*rad*eta)/10,"-----------------------------")
q3c(t0,x0,eta,rad,m)


import numpy as np
import statistics as s
from scipy.stats import pearsonr
import seaborn as sns; sns.set_theme(color_codes=True)
import statsmodels.api as sm
import matplotlib.pyplot as plt
Xs = np.array([ 0.387, 0.723, 1.00, 1.52, 5.20, 9.54, 19.2, 30.1, 39.5 ])

Ys = np.array([ 0.241, 0.615, 1.00, 1.88, 11.9, 29.5, 84.0, 165.0, 248 ])

N = 9
def cov(a, b):

    if len(a) != len(b):
        return

    a_mean = np.mean(a)
    b_mean = np.mean(b)

    sum=(np.add(a,-a_mean)*np.add(b,-b_mean)).sum()

    return sum/(len(a)-1)

print(cov(Xs,Ys))

#pearson correlation covariance(X, Y) / (stdv(X) * stdv(Y))
corr=cov(Xs,Ys)/(s.stdev(Xs) * s.stdev(Ys))
print(corr)
print('coeff: %.3f' % corr)

# ys = sns.load_dataset([Xs,Ys])
# ax = sns.regplot(x="total_bill", y="tip", data=[Xs,Ys])
# sns.scatterplot(x=horizontal_data_1, y=vertical_data_1, ax=ax[0]);
# ax = sns.regplot(x=Xs, y=Ys)

sm.qqplot(Xs, line='s')
plt.title("X distribution")

# plt.show()
sm.qqplot(Ys, line='s')
plt.title("Y distribution")
# plt.show()
corr=cov(np.log(Xs),np.log(Ys))/(s.stdev(np.log(Xs)) * s.stdev(np.log(Ys)))
bet1=corr*(s.stdev(np.log(Ys))/s.stdev(np.log(Xs)) )
print('beta improve: %.3f' % bet1)

import numpy,from scipy,import statsmodels


from sympy import symbols, S, binomial, expand

def polinomios_bernstein(f, var=symbols('x'), I=[0, 1], grado=2):
    """Genera el polinomio de Bernstein de grado n para la función f.

    Args:
        f (funcion): Función a aproximar
        var (variable, optional): Variable de f. Defaults to symbols('x').
        I (list, optional): Intervalo de aproximación. Defaults to [0, 1].
        grado (int, optional): Grado del polinomio. Defaults to 2.

    Returns:
        funcion: Polinomio de aproximación
    """
    # Los polinomios funcionan en un intervalo de 0 a 1, así que si el intervalo base es [a, b] tenemos que
    # aplicar el cambio t = (x - a)/(b - a) -> (b-a)t + a = x
    # Así transformamos f(x) en g(t)

    # Si el polinomio Bngt = sum_i^n (n i) g(i/n) t^i (1-t)^(n-1)

    vart = var * (I[1] - I[0]) + I[0]
    g = f.subs(var, vart)

    B_nft = S(0)
    for grado_i in range(grado + 1):
        poli_i = binomial(grado, grado_i) * (var ** grado_i) * ((1 - var) ** (grado - grado_i)) * g.subs(var, S(grado_i) / S(grado))
        B_nft += poli_i

    B_nf = B_nft.subs(var, S(var - S(I[0])) / S(I[1] - I[0]))

    return expand(B_nf)


# Polynomial Regression

# Importing the libraries
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

# Importing the dataset
dataset = pd.read_csv('Position_Salaries.csv')
X = dataset.iloc[:, 1:2].values
y = dataset.iloc[:, 2].values

# Splitting the dataset into the Training set and Test set
"""from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0)"""

# Feature Scaling
"""from sklearn.preprocessing import StandardScaler
sc_X = StandardScaler()
X_train = sc_X.fit_transform(X_train)
X_test = sc_X.transform(X_test)
sc_y = StandardScaler()
y_train = sc_y.fit_transform(y_train)
y_test = sc_y.transform(y_test)"""

# Fitting linear regression
from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
lin_reg.fit(X, y)

# Fitting polynomial regression
from sklearn.preprocessing import PolynomialFeatures
poly_features = PolynomialFeatures(degree = 4)
X_poly = poly_features.fit_transform(X)
poly_reg = LinearRegression()
poly_reg.fit(X_poly, y)

# Visualizing linear regression
plt.scatter(X, y, color='red')
plt.plot(X, lin_reg.predict(X), color='blue')
plt.title('Truth or Bluff (Linear Regression)')
plt.xlabel('Position level')
plt.ylabel('Salary')
plt.show()

# Visualizing polynomial regression
X_grid = np.arange(min(X), max(X), 0.1)
X_grid = X_grid.reshape((len(X_grid), 1))
plt.scatter(X, y, color='red')
plt.plot(X_grid, poly_reg.predict(poly_features.transform(X_grid)), color='blue')
plt.title('Truth or Bluff (Polynomial Regression)')
plt.xlabel('Position level')
plt.ylabel('Salary')
plt.show()

# Predicting with linear regression
lin_reg.predict(6.5)

# Predicting with polynomial regression
poly_reg.predict(poly_features.transform(6.5))

udemy/Machine Learning A-Z/Part 2 - Regression/Section 6 - Polynomial Regression/polynomial_regression.py


import numpy as np
import matplotlib.pyplot as plt
from math import sqrt
from scipy import stats
from scipy.stats import kurtosis, skew

def pdf(costh, P_mu):
    # define our probability density function
    return 0.5 * (1.0 - 1.0 / 3.0 * P_mu * costh)

def inv_cdf_pos(r, P_mu):
    # inverse of the cumulative density function
    # since we have positive and negative solutions for x(r), we have to consider both
    return 3./P_mu*(1.+np.sqrt(1.+2./3.*P_mu*(P_mu/6.+1.-2.*r)))

def inv_cdf_neg(r, P_mu):
    return 3./P_mu*(1.-np.sqrt(1.+2./3.*P_mu*(P_mu/6.+1.-2.*r)))

def inv_trans(N_measurements, P_mu):
    r = np.random.uniform(0.0, 1.0, size=N_measurements)
    data_pdf = inv_cdf_neg(r, P_mu)
    return data_pdf

def acc_rej(N_measurements, P_mu):
    x = np.random.uniform(-1.0, 1.0, size=N_measurements)

    x_axis = np.linspace(-1.0,1.0, 1000)
    fmax= np.amax(pdf(x_axis,P_mu)) #find the maximum of the function

    u = np.random.uniform(0, fmax, size= N_measurements)

    # we use a mask in order to reject the values we don't want
    data_pdf = x[u < pdf(x,P_mu)]
    return data_pdf

N_measurements=10000
P_mu=0.5

data_pdf = inv_trans(N_measurements, P_mu)
#print(len(data_pdf))
counts_bins, edges_bins, patches = plt.hist(data_pdf, bins=20, normed=True, histtype="step", color="black", linewidth = "1")

#print(counts_bins)
#print(edges_bins)

bin_width=abs(edges_bins[7]-edges_bins[8])
#print(bin_width)

#Note that the normalization is not to 1.
normalisation_bins=np.sum(counts_bins)
#print(normalisation_bins)

pdf_estimator = counts_bins/(normalisation_bins*bin_width)
#print(pdf_estimator)

center_bins=np.array([])
for i in np.arange(0,len(edges_bins)-1,1):
    center_bins=np.append(center_bins, (edges_bins[i] + edges_bins[i+1])/2.)
#print(center_bins)

plt.plot(center_bins, pdf_estimator, color = "blue", linewidth = 1, label = "pdf estimation")

x_axis = np.linspace(-1.0,1.0, 1000)
plt.plot(x_axis, pdf(x_axis,P_mu), color="red", linestyle="--", linewidth=1, label ="pdf theory")

plt.xlim(-1.,1.)
plt.ylim(0.4,0.6)
plt.xlabel(r'$cos \ \theta$')
plt.ylabel(r'$N(\cos \ \theta)$')
plt.text(-0.9, 0.41, r"$N_{events}=$" + str(N_measurements))
plt.legend(loc="best")
leg = plt.legend()
leg.get_frame().set_edgecolor('black')

plt.show()

Projects/Probability and Monte Carlo Techniques/_6_pdf_from_histogram.py


# -*- coding: utf-8 -*-
"""
Created on Tue Jun 20 10:17:03 2017

@author: fella
"""
from numpy import genfromtxt, array

def compute_error_for_given_points(b,m,points):
    totalError = 0
    for i in range(0, len(points)):
        x = points[i,0]
        y = points[i,1]
        totalError += (y - (m * x + b)) **2
    return totalError/ float(len(points))

def step_gradient(b_current, m_current, points, learning_rate):
    #gradient descent
    b_gradient = 0
    m_gradient = 0
#    b_current = new_b
#    m_current = new_m
    N = float(len(points))
    for i in range(0, len(points)):
        x = points[i,0]
        y = points[i,1]
        b_gradient += -(2/N) * (y - (m_current * x + b_current ))
        m_gradient += -(2/N) * x * (y - (m_current * x + b_current ))
    new_b = b_current - (learning_rate * b_gradient)
    new_m = m_current - (learning_rate * m_gradient)
    return [new_b, new_m]

def gradient_descent_runner(points, initial_b, initial_m, learning_rate,num_iterations):
    b = initial_b
    m = initial_m

    for i in range(num_iterations):
        b,m = step_gradient(b,m,array(points), learning_rate)
    return [b, m]

def run():
    points = genfromtxt(r'C:\Users\fella\Desktop\Documents\5. Python\Tutorials\linear_regression_live\data.csv', delimiter=',' )
    #hyperparameters
    learning_rate = 0.0001
    #y = mx + b (slope formula)
    initial_b = 0
    initial_m = 0
    num_iterations = 1000
    [b,m] = gradient_descent_runner(points, initial_b, initial_m, learning_rate,num_iterations)
    print(b)
    print(m)

if __name__ == '__main__':
    run()


# coding: utf-8

# # Calcul d'une paire de dés un peu particulière.
# - Je vais implémenter une fonction, en [Python 3](https://docs.python.org/3/), qui permettra de résoudre rapidement un problème mathématique.

# ---
# ## Exposé du problème :
# - Soit $n \geq 1$ un nombre de faces pour des dés bien équilibrés. On prendre $n = 6$ pour commencer, mais $n = 8, 10, 12, 20$ ou $n = 100$ est aussi possible.
# - On veut trouver deux ensembles d'entiers, $A$ et $B$, de tailles $n$, tels que les entiers dans $A$ soient tous [premiers](https://fr.wikipedia.org/wiki/Nombre_premier), et les entiers dans $B$ soient tous [pairs](https://fr.wikipedia.org/wiki/Parit%C3%A9_(arithm%C3%A9tique)), et on souhaite que chaque somme $a + b$ pour $a \in A$ et $b \in B$ soit encore un nombre premier.
#
# Par exemples :
# - Avec $n = 1$, prendre $A = \{3\}$ et $B = \{2\}$ fonctionne, comme $3 + 2 = 5$ est premier.
# - Avec $n = 2$, prendre $A = \{3, 5\}$ et $B = \{2, 8\}$ fonctionne, comme $3 + 2 = 5$, $3 + 8 = 11$, $5 + 2 = 7$, et $5 + 8 = 13$ sont premiers.
# - Avec $n = 6$, c'est tout de suite moins facile à trouver de tête... Allez, je triche et je donne un exemple trouvé plus bas : $A = \{5, 7, 17, 31, 61, 101\}$ et $B = \{6, 12, 36, 66, 96, 132\}$.
#
# ### Buts
# Notre but est d'abord de trouver vérifier qu'une paire $(A, B)$ donnée soit valide, puis de trouver une paire valide de taille $n$ donnée.
# On ne cherchera pas à avoir un algorithme très malin, une énumération exhaustive nous suffira.
#
#
# Ensuite on cherchera à répondre à une question plus combinatoire : peut-on trouver, à $n$ fixer, la paire valide $(A, B)$ de somme minimale (ou minimisant un certain critère) ?
# La réponse sera oui, et pas trop dure à obtenir.

# ---
# ## Présentation de ma solution numérique
# - Je vais utiliser le module python [`sympy`](http://sympy.org/) et en particulier ses fonctions du module [`sympy.ntheory`](http://docs.sympy.org/latest/modules/ntheory.html) pour avoir facilement accès à une liste de nombres premiers. En particulier, [`primerange`](http://docs.sympy.org/latest/modules/ntheory.html#sympy.ntheory.generate.primerange) sera bien pratique.
# - Je vais procéder par énumération totale avec un "doubling trick" : on cherchera toutes les paires $(A, B)$ bornées dans, disons, $[1, 100]$, puis $[1, 110]$ et ainsi de suite (avec un petit incrément), jusqu'à en trouver une qui marche.
# - Cette approche "ascendante" garantit de terminer, pour peu qu'on puisse prouver théoriquement l'existence de la solution qu'on cherche.
#
# ### Solution théorique ?
# > Je ne vais pas rentrer dans les détails, mais avec [le théorème de la progression arithmétique de Dirichlet](https://fr.wikipedia.org/wiki/Théorème_de_la_progression_arithmétique) (cf. aussi [ce document](http://perso.eleves.ens-rennes.fr/~ariffaut/Agregation/Dirichlet.pdf)), on peut montrer que pour tout nombre de faces $n \geq 1$, on peut trouver un nombre infini d'ensembles $(A, B)$ qui conviennent.
# > *Ouais, c'est balèze*.

# ----
# ### Solution numérique
# On commence avec les dépendances suivantes.
#
# > *Note :* Si vous n'avez pas installé [`sympy`](http://sympy.org/), un simple coup de `pip install sympy` suffit.

# In[1]:

from sympy import isprime
from sympy import primerange

# ---
# ## Implémentation des fonctions utilitaires requises
# D'abord, une fonction `verifie_paire` qui vérifie si une paire $(A, B)$, donnée sous forme de deux *itérateurs* (liste ou ensemble, peu importe), est valide.

# In[2]:

def verifie_paire(A, B):
    """Version efficace, qui s'arrête le plus tôt possible."""
    if A is None or B is None:
        return False
    for a in A:
        # if not isprime(a):  # Inutile car A aura été bien choisi
        #    return False
        for b in B:
            if not isprime(a + b):
                return False
    return True

# Pour visualiser un peu, on en fait une version qui parle :

# In[3]:

def verifie_paire_debug(A, B):
    """Version plus lente qui fait tous les tests, et affiche vrai ou faux pour chaque somme a + b."""
    if A is None or B is None:
        return False
    reponse = True
    for a in A:
        if not isprime(a):
            print("  - a = {:<6} est pas premier, ECHEC ...".format(a))
            reponse = False
        for b in B:
            if not isprime(a + b):
                print("  - a = {:<6} + b = {:<6} donnent {:<6} qui n'est pas premier, ECHEC ...".format(a, b, a + b))
                reponse = False
            else:
                print("  - a = {:<6} + b = {:<6} donnent {:<6} qui est bien premier, OK ...".format(a, b, a + b))
    return reponse

# Premier exemple :

# In[4]:

A = [3]
B = [2]
verifie_paire_debug(A, B)

# Second exemple :

# In[5]:

A = (3, 5)
B = (2, 8)
verifie_paire_debug(A, B)

# In[6]:

verifie_paire(A, B)

# Avec $n = 6$ :

# In[7]:

A = [5, 7, 17, 31, 61, 101]
B = [6, 12, 36, 66, 96, 132]
verifie_paire(A, B)

# Les dés vendus dans le commerce sont marqués avec ces faces :

# In[4]:

A = [2, 6, 20, 252, 266, 380]
B = [17, 41, 107, 191, 311, 347]
verifie_paire(A, B)

# ---
# On a besoin de cette fonction de combinaison :

# In[8]:

from itertools import combinations

# Ensuite, on écrit une fonction qui va énumérer *tous* les ensembles $(A, B)$ possibles et valides, avec $\max (A \cup B ) \leq M$ pour $M$ fixé.
# J'ajoute une borne inférieure $m$, valant par défaut $m = 1$, pour rester le plus générique possible, mais on ne s'en sert pas vraiment.
#
# - On récupère les candidats possibles pour $a \in A$ via `primerange` (notés $C_A$), et pour $B$ via un `range` à pas $2$ pour considérer seulement des nombres pairs (notés $B_A$),
# - Ensuite on boucle sur tous les ensembles $A$ de taille $n$ dans $C_A$, et $B$ de taille $n$ dans $C_B$, via [`itertools.combinations`](https://docs.python.org/3/library/itertools.html#itertools.combinations),
# - On garde uniquement ceux qui sont valides, et on les stocke tous.
#
# > **Attention** ça devient vite très couteux !

# In[9]:

def candidats_CA_CB(M=10, m=1):
    # Premiers entre m et M (compris), ie. P inter [|m, ..., M|]
    C_A = list(primerange(m, M + 1))
    # Nombres pairs
    if m % 2 == 0:
        C_B = list(range(m, M - 1, 2))
    else:
        C_B = list(range(m + 1, M - 1, 2))
    return C_A, C_B

# Cette première approche est très naïve, et énormément de calculs sont dédoublés (on teste les paires $(A, B)$ en faisant plein de tests pour savoir si $a + b$ est premier, c'est couteux).

# In[10]:

def enumere_toutes_les_paires(n=2, M=10, m=1, debug=False):
    C_A, C_B = candidats_CA_CB(M=M, m=m)
    if debug:
        print("C_A =", C_A)
        print("C_B =", C_B)
        print("Combinaison de n =", n, "éléments dans A =", list(combinations(C_A, n)))
        print("Combinaison de n =", n, "éléments dans B =", list(combinations(C_B, n)))
    # C_A, C_B est déjà trié, c'est cool
    all_A_B = []
    for A in combinations(C_A, n):
        if debug: print(" - A =", A)
        for B in combinations(C_B, n):
            if debug: print("     - B =", B)
            if verifie_paire(A, B):
                if debug: print("==> Une paire (A, B) de plus !")
                all_A_B.append((A, B))
    # all_A_B est aussi trié par ordre lexicographique
    return all_A_B

# On peut vérifier que les exemples donnés ci-dessus sont valides, et sont en fait les plus petits :

# In[11]:

n = 1
M = 10
enumere_toutes_les_paires(n, M, debug=True)

# In[12]:

n = 2
M = 10
enumere_toutes_les_paires(n, M, debug=True)

# On peut continuer, avec $n = 3$ et un petit majorant $M$ :

# In[13]:

n = 3
M = 20
enumere_toutes_les_paires(n, M, debug=False)

# On peut continuer, avec $n = 4$ et un petit majorant $M$ :

# In[14]:

n = 4
M = 40
get_ipython().run_line_magic('time', 'enumere_toutes_les_paires(n, M, debug=False)')

# On voit que ça commence à prendre du temps.
#
# Deux améliorations seront explorées :
# - On a pas besoin de toutes les calculer, en fait. Si on cherche juste une paire valide, on peut s'arrêter à la première trouvée.
# - On peut être malin dans la création des candidats $C_B$, plutôt que de juste prendre de nombres pairs, on prend directement des nombres $b$ tels que $a + b$ soit premier.

# ---
# ## Première optimisation
# - On n'a pas besoin d'énunumérer toutes les paires, il suffit de donner la premier trouvée.
# - Pourquoi la première ? Et bien si $C_A$ et $C_B$ sont triés, les candidats pour $(A, B)$ seront aussi triés par ordre lexicographique, et donc le premier trouvé sera le plus petit.

# In[15]:

def premiere_paire(n=2, M=10, m=1):
    C_A, C_B = candidats_CA_CB(M=M, m=m)
    # C_A, C_B est déjà trié, c'est cool
    for A in combinations(C_A, n):
        for B in combinations(C_B, n):
            if verifie_paire(A, B):
                return (A, B)
    return (None, None)

# In[16]:

n = 4
M = 40
get_ipython().run_line_magic('time', 'A, B = premiere_paire(n, M)')
print("A =", A)
print("B =", B)

# Avec $n = 5$, on commence à avoir besoin d'aller plus loin que ce majorant `M = 40` :

# In[17]:

n = 5
M = 40
get_ipython().run_line_magic('time', "A, B = premiere_paire(n, M)  # (None, None) indique qu'on a pas trouvé")
print("A =", A)
print("B =", B)

# Mais parfois on ne sait pas trop quelle valeur donner à ce majorant `M`...
#
# Une approche simple est donc d'augmenter sa valeur jusqu'à trouver une paire valide.

# In[18]:

def premiere_paire_explore_M(n=2, Ms=[10,20,30,40,50], m=1):
    for M in Ms:
        resultat = premiere_paire(n=n, M=M, m=m)
        if resultat[0] is not None:
            return resultat
    return (None, None)

# On peut faire encore mieux, en augmentant `M` automatiquement d'un certain offset $\delta_M > 0$ :

# In[19]:

def premiere_paire_augmente_M(n=2, Mmin=10, deltaM=10, m=1):
    assert isinstance(deltaM, int)
    assert deltaM >= 1
    M = Mmin
    while True:
        print("Appel à premiere_paire(n={}, M={}, m={}) ...".format(n, M, m))
        # Ce n'est pas dangereux, puisqu'on est garanti de trouver une paire qui marche
        resultat = premiere_paire(n=n, M=M, m=m)
        if resultat[0] is not None:
            print("Terminé, avec M =", M)
            return resultat
        M += deltaM

# On peut retrouver le résultat trouvé plus haut, pour $n = 2$ :

# In[20]:

n = 2
Ms = [10,20,30,40,50]
get_ipython().run_line_magic('time', 'A, B = premiere_paire_explore_M(n, Ms)')
print("A =", A)
print("B =", B)

# In[21]:

n = 4
M = 40
deltaM = 10
get_ipython().run_line_magic('time', 'A, B = premiere_paire_augmente_M(n=n, Mmin=M, deltaM=deltaM)')
print("A =", A)
print("B =", B)

# Et on peut résoudre pour $n = 5$ et $n = 6$. Mais cette approche prend beaucoup trop de temps.

# In[22]:

n = 5
M = 100
deltaM = 10
#%time A, B = premiere_paire_augmente_M(n=n, Mmin=M, deltaM=deltaM)
#print("A =", A)
#print("B =", B)

# > *Note :* Plutôt que d'écrire une fonction (qui `return` le premier résultat), on peut aussi écrire un **générateur**, qui `yield` les résultats un à un.
# > Il sera alors possible d'écrire des boucles directement comme ça :
#
# ```python
# for (A, B) in generateur_paires(n=2, M=10, m=1):
#     ...
# ```

# In[23]:

def generateur_paires(n=2, M=10, m=1):
    C_A, C_B = candidats_CA_CB(M=M, m=m)
    # C_A, C_B est déjà trié, c'est cool
    for A in combinations(C_A, n):
        for B in combinations(C_B, n):
            if verifie_paire(A, B):
                yield (A, B)

# ---
# ## Exemples

# ---
# ### Exemple avec des dés à 6 faces

# In[24]:

n = 6
M = 100
#A, B = premiere_paire(n, M)
#print("A =", A)
#print("B =", B)

# ---
# ## Autre approche, deuxième optimisation
# On a vu que l'approche *naïve* exposée ci-dessus n'est vraiment pas très efficace.
#
# On va être plus malin :
# - d'abord générer toutes paires $(a, b)$ possibles, avec $a$ et $a + b$ premiers, pour $a, b \geq M$.
# - ensuite on pourra créer la plus grande paire $(A, B)$, probablement en regardant simplement des intersections d'ensembles.

# ### Préliminaire : petite optimisation pour `isprime`
# En une seule ligne, on peut ajouter un cache ([`functools.lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache)) pour la rendre plus rapide :

# In[25]:

from functools import lru_cache

@lru_cache(maxsize=1<<10, typed=False)
def estpremier(n):
    return isprime(n)

@lru_cache(maxsize=1<<20, typed=False)
def candidats_CA_CB_cache(M=10, m=1):
    return candidats_CA_CB(M=M, m=m)

# In[26]:

import numpy.random as nr
get_ipython().run_line_magic('timeit', '[isprime(n) for n in nr.randint(1, 1<<10, 100000)]')

# In[27]:

get_ipython().run_line_magic('timeit', '[estpremier(n) for n in nr.randint(1, 1<<10, 100000)]')

# ### Générer les paires $(a, b)$ valides
# C'est assez rapide.
# - La première fonction renvoie le résultat sous forme de liste de paires $(a, b)$ avec $b$ tel que $a + b$ soit premier,
# - La seconde fonction renvoie plutôt un dictionnaire qui, à $a \in C_A$, associe tous les $b \in C_B$ tels que $a + b$.

# In[28]:

def genere_paires(M=10, m=1):
    C_A, C_B = candidats_CA_CB_cache(M=M, m=m)
    return [(a, b) for a in C_A for b in C_B if estpremier(a + b)]

def genere_dict(M=10, m=1):
    C_A, C_B = candidats_CA_CB_cache(M=M, m=m)
    return {a: [b for b in C_B if estpremier(a + b)] for a in C_A}

# In[29]:

genere_paires(10)

# In[30]:

genere_dict(10)

# On voit que le nombres de paires $(a, b)$ telle que $a + b$ soit premier et $a, b \leq M$ augmente assez vite avec $M$ :

# In[31]:

len(genere_paires(400))

# In[32]:

import matplotlib.pyplot as plt
X = list(range(10, 2000, 10))
Y = [ len(genere_paires(M)) for M in X ]
plt.plot(X, Y)
plt.show()

# ## Suite : générer une paire $(A, B)$
# - On génère les candidats $C_A, C_B$,
# - On associe à chaque $a \in C_A$ les $b \in C_B$ qui fonctionne, notés $d(a)$,
# - On garde ceux qui ont au moins $n$ valeurs associées,
# - On regarde tous les ensembles $A \subset C_A$ de taille exactement $n$,
# - Pour un $A$ donné, on construit $B$ comme $\bigcap_{a \in A} d(a)$, et on garde les $B$ de taille $\geq n$,
# - On regarde enfin tous les ensembles $B' \subset B$ de taille exactement $n$.
# - Ici, on garde juste le premier trouvé, plus bas on les garde tous.

# In[33]:

@lru_cache(maxsize=1<<10, typed=False)
def genere_A_B(n=2, M=10, m=1):
    C_A, C_B = candidats_CA_CB_cache(M=M, m=m)
    dictAB = {a: {b for b in C_B if estpremier(a + b)} for a in C_A}
    for a in C_A:
        if len(dictAB[a]) < n:
            del dictAB[a]
    # print(dictAB)
    C_A = sorted(list(dictAB.keys()))
    # C_A est déjà trié, c'est cool
    # TODO : This for loop could possibly get parallelized, using joblib.Parallel
    # It could speed up the computation by a factor of cpu_count()...
    for A in combinations(C_A, n):
        # DONE : improve this step to stop as soon as the intersection has length < n
        # B = set.intersection(*(dictAB[a] for a in A))
        B = set(dictAB[A[0]])
        for a in A[1:]:
            if len(B) < n:
                break
            B &= dictAB[a]
        if len(B) >= n:
            print(" - A =", A, "\t et B =", B, "...")
            for BB in combinations(B, n):  # On prend la 1ère
                return (list(A), list(BB))
    return (None, None)

# In[34]:

def test_genere_AB(n=2, M=10, m=1):
    A, B = genere_A_B(n=n, M=M)
    if not verifie_paire(A, B):
        print("Mauvaise paire (A, B) =", (A, B), ": elle n'est pas valable.")
        return (None, None)
    print("A =", A)
    print("B =", B)
    return (A, B)

# In[35]:

n = 2
M = 10
test_genere_AB(n=n, M=M)

# In[36]:

n = 3
M = 40
get_ipython().run_line_magic('time', 'test_genere_AB(n=n, M=M)')

# In[37]:

n = 4
M = 50
get_ipython().run_line_magic('time', 'test_genere_AB(n=n, M=M)')

# On peut comparer cette nouvelle fonction avec `premiere_paire` écrite plus haut :

# In[38]:

n = 4
M = 50
get_ipython().run_line_magic('timeit', '-n1 premiere_paire(n=n, M=M)')

# In[39]:

n = 4
M = 50
get_ipython().run_line_magic('timeit', '-n1 genere_A_B(n=n, M=M)')

# → On est effectivement bien plus rapide. Yay.

# In[40]:

n = 5
M = 100
get_ipython().run_line_magic('time', 'test_genere_AB(n=n, M=M)')

# In[41]:

n = 6
M = 100
get_ipython().run_line_magic('time', 'test_genere_AB(n=n, M=M)')

# On peut vérifier que la valeur du majorant $M$ sur les valeurs des faces ne change pas le premier $A$ trouvé, mais comme les $B$ ne sont **pas** parcourus dans un ordre lexicographique (les `set` sont non triés), on ne trouve pas le même $B$.
# Et plus $M$ est grand, plus c'est long à calculer.

# In[42]:

n = 6
M = 200
get_ipython().run_line_magic('time', 'test_genere_AB(n=n, M=M)')

# Ça marche plutôt bien, on trouve effectivement la paire qui possède le plus petit $\max A$ mais pas celle qui minimise $\sum A \cup B$ ou $\max A \cup B$...

# ## Trouver la paire de somme minimale ou de max minimum.
# - On va calculer *toutes* les paires, avec notre fonction optimisée, dans `genere_toutes_A_B`,
# - Puis on les triera (en ordre croissant) selon le critère voulu,
# - Et on renvoie la première, qui minimisera donc le critère cherché.
#
# > *Note :* en étant malin, on pourrait carrément faire mieux qu'une énumération complète. Mais la flemme.

# ### D'abord, générer toutes les paires $(A, B)$
# C'est la même fonction que `genere_A_B`, mais on les renvoie toutes au lieu de s'arrêter à la première.

# In[43]:

@lru_cache(maxsize=1<<10, typed=False)
def genere_toutes_A_B(n=2, M=10, m=1):
    C_A, C_B = candidats_CA_CB_cache(M=M, m=m)
    dictAB = {a: {b for b in C_B if estpremier(a + b)} for a in C_A}
    for a in C_A:
        if len(dictAB[a]) < n:
            del dictAB[a]
    # print(dictAB)
    C_A = sorted(list(dictAB.keys()))
    # C_A est déjà trié, c'est cool
    paires = []
    # TODO : This for loop could very easily get parallelized, using joblib.Parallel
    # It could speed up the computation by a factor of cpu_count()...
    for A in combinations(C_A, n):
        # DONE : improve this step to stop as soon as the intersection has length < n
        # B = set.intersection(*(dictAB[a] for a in A))
        B = set(dictAB[A[0]])
        for a in A[1:]:
            if len(B) < n:
                break
            B &= dictAB[a]
        if len(B) >= n:
            for BB in combinations(B, n):
                print(" - A =", A, "\t et B =", B, "...")
                # print('.', end='')  # Affiche .... au fur et à mesure de l'exécution.
                paires.append((list(A), list(BB)))
    print('')
    return paires

# On vérifie que ça fonctionne comme prévu :

# In[44]:

n = 4
M = 40
genere_toutes_A_B(n=n, M=M)

# Il suffit donc de trier selon le critère choisi.
# $$
#     \mathrm{sumsum} : (A, B) \mapsto \sum_{a \in A} a + \sum_{b \in B} b.
# $$
# $$
#     \mathrm{maxmax} : (A, B) \mapsto \max A \cup B.
# $$

# In[45]:

def sumsum(paire):
    return sum(paire[0]) + sum(paire[1])

def maxmax(paire):
    return max(max(paire[0]), max(paire[1]))

# On peut aussi prendre un critère qui combine les deux :

# In[46]:

def combine(paire):
    return (sumsum(paire) + maxmax(paire)) / 2

# Il suffit d'utiliser l'argument `key=` de la fonction [`sorted`](https://docs.python.org/3/library/functions.html#sorted) :

# In[47]:

def trie_et_prend_min(paires, key=sumsum):
    paires = sorted(paires, key=key)
    A, B = paires[0]
    assert verifie_paire(A, B)
    return (sorted(A), sorted(B))

# Pour tester :

# In[48]:

def test_trie_et_prend_min(n=2, M=10):
    paires = genere_toutes_A_B(n=n, M=M)
    print("Pour n =", n, "et M =", M, "il y a", len(paires), "paires (A, B) trouvées valides ...")

    print("- On recherche celle de somme sum(A u B) minimale ...")
    A, B = trie_et_prend_min(paires, key=sumsum)
    print("A =", A)
    print("B =", B)
    print("  Elle donne sumsum =", sumsum((A, B)), "et maxmax =", maxmax((A, B)))

    print("- On recherche celle de maximum max(A u B) minimale ...")
    A, B = trie_et_prend_min(paires, key=maxmax)
    print("A =", A)
    print("B =", B)
    print("  Elle donne sumsum =", sumsum((A, B)), "et maxmax =", maxmax((A, B)))

    print("- On recherche celle de moyenne sumsum(A,B), maxmax(A,B) minimale ...")
    A, B = trie_et_prend_min(paires, key=combine)
    print("A =", A)
    print("B =", B)
    print("  Elle donne sumsum =", sumsum((A, B)), "et maxmax =", maxmax((A, B)))

    # for key in [sumsum, maxmax, combine]:
    #    alls = [ (key(paire), (sorted(paire[0]), sorted(paire[1]))) for paire in sorted(paires, key=key) ]
    #    print(alls[:5])
    return (A, B)

# On peut refaire les mêmes exemples pour $n = 3, 4, 5$, et enfin $n = 6$.

# In[49]:

n = 3
M = 20
test_trie_et_prend_min(n, M)

# In[50]:

n = 4
M = 37
test_trie_et_prend_min(n, M)

# In[51]:

n = 5
M = 70
test_trie_et_prend_min(n, M)

# On observe que celle qui minimise `sumsum` est la même que celle qui minimise `maxmax`. Curieux ?
#
# C'est aussi le cas pour $n = 6$ :

# In[52]:

n = 6
M = 150
get_ipython().run_line_magic('time', 'test_trie_et_prend_min(n, M)')

# In[53]:

A, B = test_trie_et_prend_min(n, M)
verifie_paire_debug(A, B)

# ## Conclusion pour des dés à $6$ faces
# Avec $n = 6$ dés, la plus petite paire de valeurs $(A, B)$ trouvée est
# - A = $\{7, 13, 31, 37, 73, 97\}$
# - B = $\{6, 10, 16, 30, 66, 76\}$
#
# Elle donne $\sum A \cup B = 462$ et $\max A \cup B = 97$.
#
# Assez surprenament, c'est la paire qui minimise les deux critères à la fois (et l'unique paire qui satisfait cette propriété).

# ---
# ## Avec plus de faces ?

# ### Exemples avec des dés à 8, 10, 12 et 20 faces
# > Ça va commencer à être *très couteux*. Je recommande de ne pas faire tourner ce code sur votre machine, mais sur un serveur de calcul, par exemple !

# In[54]:

n = 7
M = 170
get_ipython().run_line_magic('time', 'test_genere_AB(n=n, M=M)')
get_ipython().run_line_magic('time', 'test_trie_et_prend_min(n, M)')

# #### Avec 8 faces

# In[55]:

n = 8
M = 440
get_ipython().run_line_magic('time', 'test_genere_AB(n=n, M=M)')
# %time test_trie_et_prend_min(n, M)

# #### Avec 10 faces

# In[ ]:

n = 10
M = 550
get_ipython().run_line_magic('time', 'test_genere_AB(n=n, M=M)')
# %time test_trie_et_prend_min(n, M)

# #### Avec 12 faces

# In[ ]:

n = 12
M = 800
get_ipython().run_line_magic('time', 'test_genere_AB(n=n, M=M)')
# %time test_trie_et_prend_min(n, M)

# #### Avec 20 faces

# In[ ]:

n = 20
M = 1000
get_ipython().run_line_magic('time', 'test_genere_AB(n=n, M=M)')
# %time test_trie_et_prend_min(n, M)

# ### Exemples avec des dés de tailles différentes
# > Il ne serait pas très compliqué d'adapter le code précédent pour considérer $A$ de taille $n$ et $B$ de taille $p$ avec possiblement $n \neq p$.
# > Je le ferai, plus tard.

# ### Exemples avec $100$ faces ?
# > Il faudrait une machine costaux, et du temps...

# ----
# # Généralisations possibles
# - Dés avec un nombre de faces différentes, i.e., $\# A \neq \# B$,
# - Plus de deux dés, i.e., considérer $A_1, \dots, A_p$ et chercher la même propriété :
#   $$ \forall a_1 \in A_1, \dots, a_p \in A_p, a_1 + \dots + a_p \in \mathcal{P}. $$
#   En imposant toujours que $A_1 \subset \mathcal{P}$, et éventuellement plus.
# - Résoudre autrement le problème initial pour ne pas avoir à donner une borné $M$ sur $\max A, B$.


import numpy as np
from numpy import inf
import time
import random

def GS_static(graph, eps, alpha, seed, method ) :

	# initial distribution
	N = graph.A.shape[0]
	y = np.zeros([N,1]); y[seed,0] = 1;

	# extract operator and coefficients
	if method == 'PageRank':
		rho = (1-alpha)
		psi = alpha;
		OP = graph.P.T
		gt = graph.pr.T
		deg = np.count_nonzero(OP, axis = 1)

	elif method == 'GammaPageRank':
		mu = (1-alpha)/alpha
		psi = -10/(2*mu + 10)
		rho = (2*mu)/(2*mu + 10)
		OP = graph.Op_shift
		gt = graph.gpr
		deg = np.count_nonzero(OP, axis = 1)

	# compute initial distribution (assuming p(0) = 0)
	p = np.zeros([N,1]);
	r = rho*y
	it = 0;
	flops = 0

	#while np.linalg.norm(r, inf) > eps :
	while (np.linalg.norm(p - gt, ord=2)/np.linalg.norm(gt,ord=2))  > eps :
		it += 1;
		ix_u = np.argmax(abs(r))
		r_u = float(r[ix_u]);
		p[ix_u] += r_u
		r[ix_u] = 0;
		r = r + np.expand_dims(psi*r_u*OP[:,ix_u],1)

		# count the flops
		flops_scaling = np.count_nonzero(OP[:,ix_u])
		flops = flops + 2*flops_scaling + np.int(deg[ix_u])

	print('---- Gauss Soutwell ----')
	print('iter =', it)
	print('flops =', flops)
	print('err =', np.linalg.norm(p - gt,ord=2)/np.linalg.norm(gt,ord=2) )
	return p, r, it, flops

def PI_static(graph, eps, alpha, seed, method) :

	# initial distribution
	N = graph.A.shape[0]
	y = np.zeros([N,1]); y[seed,0] = 1;

	# extract operator and coefficients
	if method == 'PageRank':
		rho = (1-alpha)
		psi = alpha;
		OP = graph.P.T
		gt = graph.pr.T

	elif method == 'GammaPageRank':
		mu = (1-alpha)/alpha
		psi = -10/(2*mu + 10)
		rho = (2*mu)/(2*mu + 10)
		OP = graph.Op_shift
		gt = graph.gpr

	# compute initial distribution
	p = np.zeros([N,1]);
	it = 0;
	flops = 0

	#for it in range(K):
	while (np.linalg.norm(p - gt, 2)/np.linalg.norm(gt,ord=2)) > eps :
		it += 1

		# count flops
		nnz = np.where(p > 0)[0]
		flops_dotprod = np.count_nonzero(OP[:,nnz])
		flops_scaling = np.count_nonzero(p)
		flops_addition = np.count_nonzero(OP.dot(p))
		flops = flops + flops_dotprod + flops_scaling + flops_addition

		# next iteration in approx
		p = rho*y + psi*OP.dot(p)

	print('---- Power Iteration ----')
	print('iter =', it)
	print('flops =', flops)
	print('err =', np.linalg.norm(p - gt, 2)/np.linalg.norm(gt,ord=2))

	return p, it, flops

def CP_static(graph, eps, alpha, seed, method, mode) :
	# initial distribution
	N = graph.A.shape[0];
	y = np.zeros([N,1]); y[seed,0] = 1
	flops = 0

	# Coefficient parameters
	mu = (1-alpha)/alpha
	theta = np.linspace(0,np.pi,50000+1)
	step = theta[1] - theta[0]

	# extract operator and coefficients
	if method == 'PageRank':
		Lap = (graph.D - graph.A).dot(graph.Dinv)
		OP = Lap - np.eye(N)
		gt = graph.pr.T
		filt_arg = (np.cos(theta) + 1)
		filt = np.divide(mu, mu + filt_arg)

	elif method == 'GammaPageRank':
		OP = graph.Op_shift
		gt = graph.gpr
		filt_arg = (10/2)*(np.cos(theta) + 1)
		filt = np.divide(mu, mu + filt_arg)

	# coefficients
	tmp1 = np.multiply(np.cos(0*theta),filt*step); tmp1[0]=tmp1[0]/2; tmp1[-1]=tmp1[-1]/2;
	tmp2 = np.multiply(np.cos(1*theta),filt*step); tmp2[0]=tmp2[0]/2; tmp2[-1]=tmp2[-1]/2;
	coef1 = (2/np.pi)*np.sum(tmp1)
	coef2 = (2/np.pi)*np.sum(tmp2)

	# Polynomial elements
	polyTerm_2back = np.array(y)
	polyTerm_1back = np.array(OP).dot(y)
	nnz = np.where(y != 0)[0]
	flops = flops + np.count_nonzero(OP[:,nnz])

        # Chebyshev approximation
	Cheby_approximation_prev = 0.5*coef1*polyTerm_2back + coef2*polyTerm_1back;
	Cheby_approximation_curr = np.array(Cheby_approximation_prev)
	flops = flops + 2*np.count_nonzero(polyTerm_1back)

	#for it in range(2, hops-1):
	it = 2;
	activeNodes = np.where(graph.clust_memb > 0)[0]

	if mode == 'FixedError':
		while (np.linalg.norm(Cheby_approximation_curr[activeNodes] - gt[activeNodes], ord=2)/np.linalg.norm(gt[activeNodes],ord=2)) > eps :

			# Chebyshev coefficient
			tmp = np.array(np.multiply(np.cos(it*theta),filt*step)); tmp[0]=tmp[0]/2; tmp[-1]=tmp[-1]/2;
			coef_curr = (2/np.pi)*np.sum(tmp);

			# Current polynomial term
			polyTerm_curr = 2*(OP).dot(polyTerm_1back) - polyTerm_2back;
			nnz = np.where(polyTerm_1back != 0)[0]
			flops = flops + np.count_nonzero(OP[:,nnz]) + np.count_nonzero(OP.dot(polyTerm_1back))  + np.count_nonzero(polyTerm_1back) + np.count_nonzero(polyTerm_2back)

			# Chebyshev approximation
			Cheby_approximation_curr = np.array(Cheby_approximation_prev + coef_curr*polyTerm_curr);
			flops = flops + 2*np.count_nonzero(polyTerm_curr)

			# Update
			polyTerm_2back = np.array(polyTerm_1back);
			polyTerm_1back = np.array(polyTerm_curr);
			Cheby_approximation_prev = np.array(Cheby_approximation_curr);
			it += 1
	elif mode == 'FixedFlops':
		while flops < eps:
			# Chebyshev coefficient
			tmp = np.array(np.multiply(np.cos(it*theta),filt*step)); tmp[0]=tmp[0]/2; tmp[-1]=tmp[-1]/2;
			coef_curr = (2/np.pi)*np.sum(tmp);

			# Current polynomial term
			polyTerm_curr = 2*(OP).dot(polyTerm_1back) - polyTerm_2back;
			nnz = np.where(polyTerm_1back != 0)[0]
			flops = flops + np.count_nonzero(OP[:,nnz]) + np.count_nonzero(OP.dot(polyTerm_1back))  + np.count_nonzero(polyTerm_1back) + np.count_nonzero(polyTerm_2back)

			# Chebyshev approximation
			Cheby_approximation_curr = np.array(Cheby_approximation_prev + coef_curr*polyTerm_curr);
			flops = flops + 2*np.count_nonzero(polyTerm_curr)

			# Update
			polyTerm_2back = np.array(polyTerm_1back);
			polyTerm_1back = np.array(polyTerm_curr);
			Cheby_approximation_prev = np.array(Cheby_approximation_curr);
			it += 1

	p = Cheby_approximation_curr
	err = np.linalg.norm(p[activeNodes] - gt[activeNodes],ord=2)/np.linalg.norm(gt[activeNodes],ord=2)
	print('---- Chebyshev ----')
	print('iter =', it)
	print('flops =', flops)
	print('err =', err )

	return p, it, flops, err


#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np

def cart2bary(verts, pts):
    """ retrns barycentric coords of pt on a tri w/ vertices verts
    @param verts  tuple of tuples specifying verts
    @param pt  numpy array of point on triangle shape (2, npts)
    """
    npts = pts.shape[1]
    _lambda = np.zeros((3, npts))
    (x1, y1), (x2, y2), (x3, y3) = verts
    T = np.array([[x1 - x3, x2 - x3],
                  [y1 - y3, y2 - y3]])
    r = np.copy(pts)
    r[0,:] -= x3
    r[1,:] -= y3

    _lambda[:2, :] = np.linalg.solve(T, r)
    _lambda[2, :] = 1 - _lambda[0,:] - _lambda[1,:]
    return _lambda

def bary2cart(verts, _lambda):
    """converts from barycentric to cartesian coordinates
    @param verts  tuple of tuples specifying triangle vertices
    @param _lambda array of barycentric points (3, npts)
    """
    npts = _lambda.shape[1]
    λ1, λ2, λ3 = _lambda[0,:], _lambda[1,:], _lambda[2,:]
    x, y = np.zeros(npts), np.zeros(npts)
    (x1, y1), (x2, y2), (x3, y3) = verts
    x = λ1*x1 + λ2*x2 + λ3*x3
    y = λ1*y1 + λ2*y2 + λ3*y3
    return x, y

def uniform_bary_coords(p):
    """ generate bary coords of uniform nodal pts for tri of order p"""
    N, Np = p, int((p+1)*(p+2)/2)
    node = 0
    bary = np.zeros((3, Np))
    for i in range(N+1):
        for j in range(N+1-i):
            λ1, λ2 = i/N, j/N
            λ3 = 1 - λ1 - λ2
            bary[:, node] = np.asarray([λ1, λ2, λ3])
            node += 1
    return bary


"""

finite_difference_coefficients.py
TT.py
Determine the coefficients to a finite differencing scheme

"""

import numpy as np
import math

def finite_difference_coefficients(x, deriv):
    """
    Find the coefficients to a finite differencing scheme.
    i.e. find the coefficients [c1, c2, c3] that are appropriate for the 2nd order
    centered difference approximation to the first derivative:

    (du/dx)[j] = 1/dx * (c1*u[i-j] + c2*u[j] + c3*u[j+1]) + error terms

    Parameters
    ----------
    x: numpy array:
        Vector containing relative distances to sampling points
    deriv: int
        Order of the derivative to approximate

    Returns
    -------
    coefficients : numpy array
        Coefficients requested
    coefficient_table : numpy array
        Output matrix showing the Taylor table coefficients
    error_order : lowest order of higher order terms
                  (order of accuracy (e.g. O(dx**2) is 2nd order))
    """

    N = len(x)
    coefficient_table = np.zeros((N, N + 7))

    #Do for each coefficient of the Taylor Expansion
    #Expand out 6 extra terms for higher accuracies at higher derivatives
    for j in range(N):
        for n in range(N + 7):
            coefficient_table[j, n] = -1.0 / math.factorial(n) * x[j]**n

    #Allocate derivative
    d = np.zeros((N, 1))
    d[deriv] = -1

    #Calculate the coefficients
    c = np.linalg.lstsq(coefficient_table[:, 0:N].T, d, rcond=-1)[0]

    #Set any small value to zeros
    for i in range(len(c)):
        if np.abs(c[i]) < 1e-10:
            c[i] = 0

    """
    Determine the order of accuracy
    Multiply the Taylor table of coefficients by the finite difference
    coefficients and add up all like terms.
    Terms that don't add to zero are the objective derivative and
    the residual higher order terms
    """

    a = (c.T @ coefficient_table)[0]

    #Values closer to zero are zero
    for i in range(len(a)):
        if np.abs(a[i]) < 1e-5:
            a[i] = 0

    """
    Location of first nonzero term is the order of derivative you are approximating.
    Second nonzero term tells the lowest order of the higher order terms
    """

    position = np.argwhere(a)[:2]

    error_order = position[1][0] - deriv

    coefficient_table = coefficient_table[:, 0:N]

    return c.T[0]


"""
David Kartchner
Project Euler Problem 27--Quadratic Primes
May 21, 2016
"""

import numpy as np

#find all primes under n
def primes_below_n(n):
	primes = np.array([i for i in xrange(2, n+1)])
	k = int(np.sqrt(n))
	#we only need to use primes less than sqrt n to eliminate composite numbers
	seive = np.array([i for i in xrange(2, k+1)])
	#j is indexing number
	j = 0

	while j < len(seive):
		primes = primes[(primes <= seive[j])|(primes%seive[j] != 0)]
		seive = seive[(seive%seive[j]!=0)|(seive <= seive[j])]
		j+=1
	return primes

def quadratic_primes():
    max_seq = 0
    best_a=1
    best_b=41
    possible_b = primes_below_n(1000)
    divisors = primes_below_n(2000)
    for a in xrange(-1000,1001):
        #if a%10==0:
            #print a
        for b in possible_b:
            n=0
            prime=True
            while prime:
                num = n**2 + a*n + b
                for i in divisors:

                    if num%i == 0 and num!=i:
                        prime=False
                if prime:
                    n+=1
                if n > max_seq:
                    print "New best a and b:", a, b
                    print "New best n:", n
                    max_seq = n
                    best_a = a
                    best_b = b
    print "Best Product:", best_b*best_a

if __name__ == '__main__':
    quadratic_primes()


import numpy as np
from scipy.sparse import csr_matrix

DEFAULT_H = 0.1

class HuberSVM():
    @staticmethod
    def loss(theta, x, y, lambda_param=None, h=DEFAULT_H):
        """Loss function for Huber SVM without regularization"""

        z = y * x.dot(theta)

        losses = np.zeros(z.shape)
        high_inds = z < 1 - h  # since we want 1 - z > h
        losses[high_inds] = 1 - z[high_inds]

        low_inds = z > 1 + h  # since we want 1 - z < -h
        mid_inds = ~(high_inds+low_inds)
        num = 1 - z[mid_inds]
        losses[mid_inds] = pow(num,2)/(4*h) + num/2 + h/4
        loss = np.sum(losses)/x.shape[0]

        return loss

    @staticmethod
    def gradient(theta, x, y, lambda_param=None, h=DEFAULT_H):
        """
        Gradient function for Huber SVM without regularization
        Based on the above Huber SVM
        """

        z = y * x.dot(theta)

        high_inds = z < 1 - h  # since we want 1 - z > h
        low_inds = z > 1 + h  # since we want 1 - z < -h
        mid_inds = ~(high_inds+low_inds)
        num = 1 - z[mid_inds]

        grads = np.zeros((x.shape[0],x.shape[1]))

        if np.sum(high_inds) > 0:
          x_result = x[np.where(high_inds==1)[0],:]
          if isinstance(x_result, csr_matrix):
              x_result = x_result.toarray()

          grads[np.where(high_inds==1)[0],:] = (-1
                                                * y[high_inds].reshape(
                                                    (y[high_inds].shape[0]),1)
                                                * x_result)
        if np.sum(mid_inds) > 0:
          x_result = x[np.where(mid_inds==1)[0],:]
          if isinstance(x_result, csr_matrix):
              x_result = x_result.toarray()

          grads[np.where(mid_inds==1)[0],:] = ((-1
                                                * y[mid_inds].reshape(
                                                    (y[mid_inds].shape[0]),1)
                                                * x_result)
                                               * (num/(2*h) + 0.5).reshape(
                                                   (y[mid_inds].shape[0]),1))
        grad = np.mean(grads, 0)

        return grad

class HuberSVMRegular():
    @staticmethod
    def loss(theta, x, y, lambda_param, h=DEFAULT_H):
        regularization = (lambda_param/2) * np.sum(theta*theta)
        return HuberSVM.loss(theta, x, y, h) + regularization

    @staticmethod
    def gradient(theta, x, y, lambda_param, h=DEFAULT_H):
        regularization = lambda_param * theta
        return HuberSVM.gradient(theta, x, y, h) + regularization


#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time     : 2018/11/19 15:30
# @Author   : Iydon
# @File     : course7.3.py

import numpy as np

def jacobi_method(A, b):
	A_,b_ = A.copy(),b.copy()
	diag  = np.diag(A_)
	shap  = A.shape
	A_ = np.diag(diag) - A_
	x_ = np.zeros(b.shape)
	for i in range(shap[0]):
		A_[i,:] /= diag[i]
		b_[i,:] /= diag[i]
	while True:
		yield x_
		x_ = A_*x_ + b_

A = np.matrix([[10.,-1,2,0],[-1,11,-1,3],[2,-1,10,-1],[0,3,-1,8]])
b = np.matrix([[6.],[25],[-11],[15]])
result = jacobi_method(A, b)
t = 0
for r in result:
	print(t, r.T, "\n")
	t += 1
	if t > 10: break


import numpy as np

def softmax(predictions):
    """
    Computes probabilities from scores

    Arguments:
      predictions, np array, shape is either (N) or (batch_size, N) -
        classifier output

    Returns:
      probs, np array of the same shape as predictions -
        probability for every class, 0..1
    """

    preds = np.array(predictions.copy())

    if preds.ndim > 1:
        preds -= np.max(preds, axis=1).reshape(-1, 1)
        proba = np.exp(preds) / np.sum(np.exp(preds), axis=1).reshape(-1, 1)
    else:
        preds -= np.max(preds)
        proba = np.exp(preds) / np.sum(np.exp(preds))

    return proba

def cross_entropy_loss(probs, target_index):
    """
    Computes cross-entropy loss

    Arguments:
      probs: np array, shape is either (N) or (batch_size, N) -
        probabilities for every class
      target_index: np array of int, shape is (1) or (batch_size) -
        index of the true class for given sample(s)

    Returns:
      loss: single value
    """

    if probs.ndim > 1:
        true_probs = probs[np.arange(len(target_index)), target_index.reshape(-1)]
    else:
        true_probs = probs[target_index]

    loss = np.sum(-np.log(true_probs))
    return loss

def softmax_with_cross_entropy(predictions, target_index):
    """
    Computes softmax and cross-entropy loss for model predictions,
    including the gradient

    Arguments:
      predictions: np array, shape is either (N) or (batch_size, N) -
        classifier output
      target_index: np array of int, shape is (1) or (batch_size) -
        index of the true class for given sample(s)

    Returns:
      loss: single value - cross-entropy loss
      dprediction: np array same shape as predictions - gradient of predictions by loss value
    """

    # TODO implement softmax with cross-entropy

    if predictions.ndim > 1:
        mask = np.zeros(predictions.shape)
        mask[np.arange(len(target_index)), target_index.reshape(-1)] = 1
    else:
        mask = np.zeros(predictions.shape)
        mask[target_index] = 1

    probs = softmax(predictions.copy())
    loss = cross_entropy_loss(probs, target_index)
    dpredictions = probs - mask

    return loss, dpredictions

def l2_regularization(W, reg_strength):
    """
    Computes L2 regularization loss on weights and its gradient

    Arguments:
      W: np array, weights
      reg_strength: float value

    Returns:
      loss: single value - l2 regularization loss
      gradient: np.array same shape as W - gradient of weight by l2 loss
    """

    w = W.copy()
    loss = np.sum(w * w) * reg_strength
    grad = w * 2 * reg_strength

    return loss, grad

def linear_softmax(X, W, target_index):
    """
    Performs linear classification and returns loss and gradient over W

    Arguments:
      X, np array, shape (num_batch, num_features) - batch of images
      W, np array, shape (num_features, classes) - weights
      target_index, np array, shape (num_batch) - index of target classes

    Returns:
      loss, single value - cross-entropy loss
      gradient, np.array same shape as W - gradient of weight by loss
    """

    predictions = np.dot(X, W)
    loss, dpred = softmax_with_cross_entropy(predictions, target_index)
    dW = X.T.dot(dpred)
    return loss, dW

class LinearSoftmaxClassifier():
    def __init__(self):
        self.W = None

    def fit(self, X, y, batch_size=100, learning_rate=1e-7, reg=1e-5, epochs=1):
        """
        Trains linear classifier

        Arguments:
          X, np array (num_samples, num_features) - training data
          y, np array of int (num_samples) - labels
          batch_size, int - batch size to use
          learning_rate, float - learning rate for gradient descent
          reg, float - L2 regularization strength
          epochs, int - number of epochs
        """

        num_train = X.shape[0]
        num_features = X.shape[1]
        num_classes = np.max(y)+1
        if self.W is None:
            self.W = 0.001 * np.random.randn(num_features, num_classes)

        loss_history = []
        for epoch in range(epochs):
            loss = 0
            shuffled_indices = np.arange(num_train)
            np.random.shuffle(shuffled_indices)
            sections = np.arange(batch_size, num_train, batch_size)
            batches_indices = np.array_split(shuffled_indices, sections)

            for batch in batches_indices:
                loss_softmax, dw_softmax = linear_softmax(X[batch], self.W, y[batch])
                loss_reg, dw_reg = l2_regularization(self.W, reg)
                loss += (loss_softmax + loss_reg)
                self.W -= learning_rate * (dw_softmax + dw_reg)
            loss_history.append(loss)
            # end
            if epoch % 100 == 0:
                print("Epoch %i, loss: %f" % (epoch, loss))

        return loss_history

    def predict(self, X):
        """
        Produces classifier predictions on the set

        Arguments:
          X, np array (test_samples, num_features)

        Returns:
          y_pred, np.array of int (test_samples)
        """

        y_pred = np.zeros(X.shape[0], dtype=np.int)

        # TODO Implement class prediction
        # Your final implementation shouldn't have any loops
        pred = np.argmax(softmax(X.dot(self.W)), axis=1)

        return pred


from statistics import mean
import numpy as np
import matplotlib.pyplot as plt
import random

# xs = np.array([1, 2, 3, 4, 5, 6], dtype=np.float64)
# ys = np.array([5, 4, 6, 5, 6, 7], dtype=np.float64)

# This will be used to create random data sets
def create_dataset(size, varience, step=2, correlation=False):
    val = 0
    ys = []

    # Assign random values to the ys list
    for i in range(size):
        y = val + random.randrange(-varience, varience)
        ys.append(y)

        if correlation and correlation == 'pos':
            val += step
        elif correlation and correlation == 'neg':
            val -= step

    xs = [i for i in range(len(ys))]

    return np.array(xs, dtype=np.float64), np.array(ys, dtype=np.float64)

def best_fit_slope_and_intercept(xs, ys):
    m = (((mean(xs) * mean(ys)) - mean(xs * ys)) /
         (mean(xs) * mean(xs) - mean(xs * xs)))

    b = mean(ys) - m * mean(xs)
    return m, b

def squared_error(ys_orig, ys_line):
    return sum((ys_line - ys_orig) ** 2)

# This is meant to tell us how good of a fit the line
# We made is
def coefficient_of_determination(ys_orig, ys_line):
    y_mean_line = [mean(ys_orig) for y in ys_orig]
    squared_error_regr = squared_error(ys_orig, ys_line)
    squared_error_y_mean = squared_error(ys_orig, y_mean_line)

    return 1 - (squared_error_regr / squared_error_y_mean)

# Create our data set
xs, ys = create_dataset(1000, 100, 2, correlation='pos')

# Make the line of best fit
m, b = best_fit_slope_and_intercept(xs, ys)
regression_line = []

for x in xs:
    regression_line.append((m * x) + b)

# This is how we know how good of a fit the line actually is
r_squared = coefficient_of_determination(ys, regression_line)
print(r_squared)

# Show the graph
plt.scatter(xs, ys, s=10)
plt.plot(xs, regression_line)
plt.show()


"""
This code implements a K-means and EM Gaussian mixture models per week 8 assignment of the machine learning module part of Columbia University Micromaster programme in AI.
Written using Python 3.7 for running on Vocareum
Version run on Vocareum for grading (all docs removed to avoid issues with grading platform)

Execute as follows:
$ python3 hw3_clustering.py X.csv
"""

# builtin modules
import sys
import os
import math
from random import randrange
import functools
import operator
import requests
import psutil

# 3rd party modules
import numpy as np
import pandas as pd
import scipy as sp
from scipy.cluster.vq import kmeans2
from scipy.stats import multivariate_normal
from scipy.spatial.distance import cdist
from scipy.special import logsumexp
from scipy import stats

def KMeans(data, k, iterations, **kwargs):
    #data = data.to_numpy() # Convert dataframe to np array
    centroids_list = []
    labels_list = []

    for i in range(iterations):
        centroids, label = kmeans2(data, k, iter = i+1, minit='points')
        centroids_list.append(centroids)
        labels_list.append(label)
        filename = "centroids-" + str(i+1) + ".csv" #"i" would be each iteration
        if 'path' in kwargs:
            path = kwargs['path']
            filepath = os.path.join(path, filename)
            np.savetxt(filepath, centroids, delimiter=",")
        else:
            path = os.getcwd() # os.path.join(os.getcwd(), "outputs")
            filepath = os.path.join(path, filename)
            np.savetxt(filepath, centroids, delimiter=",")

    return centroids_list, labels_list

def calculate_mean_covariance(data, centroids, labels):

    # initialize the output ndarrays with zeros, for filling up in loops below
    d = data.shape[1]
    k = len(centroids[0]) # to take number of clusters directly from KMeans
    initial_pi = np.zeros(k)
    initial_mean = np.zeros((k, d))
    initial_sigma = np.zeros((k, d, d))

    # ensure data is dataframe from np.ndarray
    data = pd.DataFrame(data=data)

    # get the number of iterations from the lenght of centroids (or labels)
    iterations = len(centroids)

    for i in range(iterations):
        iter_centroid = centroids[i]
        iter_labels = labels[i]
        # initialize counter to organize outputs per cluster k (counter = cluster_labels if the latter are int type ranging from 0 to k)
        counter=0
        for cluster_label in np.unique(iter_labels):
            # returns indices of rows estimated to belong to the cluster
            ids = np.where(iter_labels == cluster_label)[0]
            # calculate pi = nk / n for cluster k
            nk = data.iloc[ids].shape[0] # number of data points in current gaussian/cluster
            n = data.shape[0] # total number of points/rows in dataset
            initial_pi[counter] = nk / n

            # calculate mean (mu) of points estimated to be in cluster k (k)
            initial_mean[counter,:] = np.mean(data.iloc[ids], axis = 0)
            de_meaned = data.iloc[ids] - initial_mean[counter,:]

            # calculate covariance (sigma) of points estimated to be in cluster k (k)
            initial_sigma[counter, :, :] = np.dot(initial_pi[counter] * de_meaned.T, de_meaned) / nk

            counter+=1

        #assert np.sum(initial_pi) == 1

    return (initial_pi, initial_mean, initial_sigma)

def initialise_parameters(data, k, iterations):

    centroids, labels =  KMeans(data, k, iterations)

    (initial_pi, initial_mean, initial_sigma) = calculate_mean_covariance(data, centroids, labels)

    return (initial_pi, initial_mean, initial_sigma)

def e_step(data, pi, mu, sigma):

    n = data.shape[0]
    k = len(pi)
    gamma = np.zeros((n, k))

    # convert np.nan to float(0.0)
    pi = np.nan_to_num(pi)
    mu = np.nan_to_num(mu)
    sigma = np.nan_to_num(sigma)

    x = data #.to_numpy() # Convert dataframe to np array

    for cluster in range(k):
        # Posterior Distribution using Bayes Rule
        gamma[ : , cluster] = pi[cluster] * multivariate_normal(mean=mu[cluster,:], cov=sigma[cluster], allow_singular=True).pdf(x)

    gamma = np.nan_to_num(gamma) # convert np.nan to float(0.0)
    # normalize across columns to make a valid probability
    gamma_norm = np.sum(gamma, axis=1)[ : , np.newaxis]
    gamma_norm = np.nan_to_num(gamma_norm)

    # avoid issues with divide by zero of ndarray
    gamma = np.divide(gamma, gamma_norm, out=np.zeros_like(gamma), where=gamma_norm!=0)
    #gamma /= gamma_norm

    return np.nan_to_num(gamma)

def m_step(data, gamma, sigma):

    n = data.shape[0] # number of datapoints (rows in the dataset), equivalent of gamma.shape[0]
    k = gamma.shape[1] # number of clusters
    d = data.shape[1] # number of features (columns in the dataset)

    # convert np.nan to float(0.0)
    gamma = np.nan_to_num(gamma)
    sigma = np.nan_to_num(sigma)

    # calculate pi and mu for each Gaussian
    pi = np.mean(gamma, axis = 0)
        # avoid issues with divide by zero of ndarray
    #mu = dividend / divisor
    dividend = np.dot(gamma.T, data)
    divisor = np.sum(gamma, axis = 0)[:,np.newaxis]
    mu = np.divide(dividend, divisor, out=np.zeros_like(dividend), where=divisor!=0)

    x = data #.to_numpy() # Convert dataframe to np array

    # update sigma for each Gaussian
    for cluster in range(k):
        x = x - mu[cluster, :] # (shape: n, d)
        gamma_diag = np.diag(gamma[: , cluster])
        x_mu = np.matrix(x)
        gamma_diag = np.matrix(gamma_diag)

        sigma_cluster = x.T * gamma_diag * x
        gamma = np.nan_to_num(gamma) # convert np.nan to float(0.0)
        #sigma = dividend / divisor
        dividend = (sigma_cluster)
        divisor = np.sum(gamma, axis = 0)[:,np.newaxis][cluster]
        sigma[cluster,:,:] = np.divide(dividend, divisor, out=np.zeros_like(dividend), where=divisor!=0)

    return pi, mu, sigma

def predict(data, pi, mu, sigma, k):

    n = data.shape[0] # number of datapoints (rows in the dataset)
    labels = np.zeros((n, k))

    # convert np.nan to float(0.0)
    pi = np.nan_to_num(pi)
    mu = np.nan_to_num(mu)
    sigma = np.nan_to_num(sigma)

    #x = data.to_numpy() # Convert dataframe to np array

    for cluster in range(k):
        labels [:,cluster] = pi[cluster] * multivariate_normal(mean=mu[cluster,:], cov=sigma[cluster], allow_singular=True).pdf(data)
    labels  = labels .argmax(1)

    return labels

def EMGMM(data, k, iterations, **kwargs):

    if 'tol' in kwargs:
        tol = kwargs['tol']
    else:
        tol = 1e-6

    d = data.shape[1] # number of features (columns in the dataset)
    n = data.shape[0] # number of datapoints (rows in the dataset)

    x = data #.to_numpy() # Convert dataframe to np array

    pi, mu, sigma =  initialise_parameters(data, k, iterations)

    for i in range(iterations):
        gamma  = e_step(data, pi, mu, sigma)
        pi, mu, sigma = m_step(data, gamma, sigma)
        filename = "pi-" + str(i + 1) + ".csv"
        np.savetxt(filename, pi, delimiter=",") #this must be done at every iteration
        filename = "mu-" + str(i + 1) + ".csv"
        np.savetxt(filename, mu, delimiter=",")  #this must be done at every iteration
        for cluster in range(k): #k is the number of clusters
            filename = "Sigma-" + str(cluster + 1) + "-" + str(i + 1) + ".csv" #this must be done k times for each iteration
            np.savetxt(filename, sigma[cluster], delimiter=",")

    predicted_labels = predict(x, pi, mu, sigma, k)

    # convert np.nan to float(0.0)
    pi = np.nan_to_num(pi)
    mu = np.nan_to_num(mu)
    sigma = np.nan_to_num(sigma)

    # compute centers as point of highest density of distribution
    centroids = np.zeros((k,d))

    for cluster in range(k):
        density = multivariate_normal(mean=mu[cluster], cov=sigma[cluster], allow_singular=True).logpdf(x)
        centroids[cluster, :] = x[np.argmax(density)]

    return centroids, predicted_labels

def main():
    #Uncomment next line when running in Vocareum
    data=np.genfromtxt(sys.argv[1], delimiter = ',', skip_header=1)
    #Run KMeans to get clusters in the data and a csv of clusters per iteration
    centroids, labels = KMeans(data, 5, 10)
    #Run EMGMM to output the required csv files plus the predicted_labels
    c_emgmm, l_emgmm = EMGMM(data, 5, 10)

if __name__ == '__main__':
    main()


import numpy as np

class Activation:
    @staticmethod
    def forward(x):
        raise NotImplementedError

    @staticmethod
    def backward(dout, cache):
        raise NotImplementedError

class Sigmoid(Activation):
    """
    In the forward pass, sigmoid(x) = 1 / (1 + exp(-x))
    In the backward pass, sigmoid'(x) = sigmoid(x) * (1 - sigmoid(x))
    """
    @staticmethod
    def forward(x):
        output = 1. / (1. + np.exp(-x))
        cache = output
        return output, cache

    @staticmethod
    def backward(dout, cache):
        grad = cache * (1 - cache)

        return dout * grad

class Relu(Activation):
    @staticmethod
    def forward(x):
        output = np.maximum(0, x)
        cache = x

        return output, cache

    @staticmethod
    def backward(dout, cache):
        grads = np.array(dout, copy=True)
        grads[cache < 0] = 0

        return grads

class Softmax(Activation):
    """
    Since CrossEntropyLoss is easier and efficient to implement,
    I will not implement a single Softmax class this time.
    """
    @staticmethod
    def forward(x):
        raise NotImplementedError

    @staticmethod
    def backward(dout, cache):
        raise NotImplementedError

if __name__ == '__main__':
    x = np.array([0.4, 1, -20, 48])

    # sigmoid test
    y, cache= Sigmoid.forward(x)
    grads = Sigmoid.backward(1, cache)

    f = lambda x: 1. / (1. + np.exp(-x))
    g = lambda x: (f(x + 0.0001) - f(x - 0.0001)) / 0.0002

    print(f(x), g(x))
    print(y, grads)


# 2017 JA
import numpy as np

def lorenz63(x_0,tmax,paramout=None):
    """Evolution of the Lorenz 1963 3-variable model.

    Inputs:  - x_0, an array containing the initial position
             - tmax, the maximum time.  Should be a multiple of the
               timestep 0.01.
    Outputs: - t, the time array
             - xt, the nature run [len(t) x 3]"""
    # The time array
    tstep = 0.01
    t = np.arange(0,tmax+tstep/2,tstep)
    # Note that with this step size, 8 steps are approximately linear windows
    # Now, define the vectors for space vars. They're organized in an
    # array of 3 columns [x,y,z].
    xt = np.empty((len(t),3))
    xt.fill(np.nan)
    xt[0,:] = x_0

    # The cycle containing time integration
    for i in range(len(t)-1): # for each time step
        Varsold = xt[i,:]
        # We integrate using Runge-Kutta-4
        Varsnew = rk4(Varsold,tstep,paramout)
        xt[i+1,:] = Varsnew

    return t,xt

## Auxiliary functions
def rk4(Varsold,tstep,paramout=None):
    "This function contains the RK4 routine."
    k1 = f(Varsold,paramout)
    k2 = f(Varsold+1/2.0*tstep*k1,paramout)
    k3 = f(Varsold+1/2.0*tstep*k2,paramout)
    k4 = f(Varsold+tstep*k3,paramout)
    Varsnew = Varsold + 1/6.0*tstep*(k1+2*k2+2*k3+k4)
    return Varsnew

def f(x,paramout=None):
    "This function contains the actual L63 model."
    # The parameters
    if np.all(paramout)==None:
     sigma = 10.0;     b = 8/3.0;    r = 28.0
    else:
     sigma = paramout[0];  b = paramout[1];  r = paramout[2]

    # Initialize
    k = np.empty_like(x)
    k.fill(np.nan)
    # The Lorenz equations
    k[0] = sigma*(x[1]-x[0])
    k[1] = x[0]*(r-x[2])-x[1]
    k[2] = x[0]*x[1]-b*x[2]
    return k

#############################
def lorenz63_stoch(x_0,tmax,sqrtQ=None):
 #L63 with model error
 # The time array
 tstep = 0.01
 t = np.arange(0,tmax+tstep/2,tstep)
 Nsteps = np.size(t)
 Nx = np.size(x_0)
 xt = np.empty((Nsteps,Nx)); xt.fill(np.nan)
 xt[0,:] = x_0
 # The cycle containing time integration
 for i in range(Nsteps-1): # for each time step
  Varsold = xt[i,:]
  Varsnew = rk4(Varsold,tstep)
  if np.all(sqrtQ)==None:
   jump = [0,0,0]
  else:
   jump = np.dot(sqrtQ,np.random.randn(Nx))
  x_aux = Varsnew + jump
  xt[i+1,:] = x_aux
 del i
 return t,xt


import pandas as pd
import plotly.express

from IMLearn.learners import UnivariateGaussian, MultivariateGaussian
import numpy as np
import plotly.graph_objects as go
import plotly.io as pio
pio.templates.default = "simple_white"

def test_univariate_gaussian():
    # Question 1 - Draw samples and print fitted model
    X = np.random.normal(10, 1, 1000)
    estimator = UnivariateGaussian()
    estimator = estimator.fit(X)
    print(f'({estimator.mu_}, {estimator.var_})')

    # Question 2 - Empirically showing sample mean is consistent
    df = pd.DataFrame(columns=['Sample Size', 'Distance from Real Expectation'])
    for sample_size in range(10, 1001, 10):
        sample = X[:sample_size]
        model = UnivariateGaussian(biased_var=False)
        model = model.fit(sample)
        distance = abs(model.mu_ - 10)
        df = pd.concat([df, pd.DataFrame({'Sample Size': [sample_size],
                                          'Distance from Real Expectation': [distance]})])
    plotly.express.bar(df, x='Sample Size', y='Distance from Real Expectation',
                       title='Distance from Actual Expectation as a Function '
                             'of Sample Size').show()

    # Question 3 - Plotting Empirical PDF of fitted model
    pdf = estimator.pdf(X)
    df = pd.DataFrame(zip(X, pdf), columns=['Sample Value', 'Probability'])
    plotly.express.scatter(df, x='Sample Value', y='Probability',
                           title='PDF of Fitted Model').show()

def test_multivariate_gaussian():
    # Question 4 - Draw samples and print fitted model
    mu = np.array([0, 0, 4, 0])
    sigma = np.array([[1, 0.2, 0, 0.5],
                       [0.2, 2, 0, 0],
                       [0, 0, 1, 0],
                       [0.5, 0, 0, 1]])
    X = np.random.multivariate_normal(mu, sigma, 1000)
    estimator = MultivariateGaussian()
    estimator = estimator.fit(X)
    print(estimator.mu_)
    print(estimator.cov_)

    # Question 5 - Likelihood evaluation
    res = []
    for f1 in np.linspace(-10, 10, 200):
        for f3 in np.linspace(-10, 10, 200):
            mu = np.array([f1, 0, f3, 0])
            log_likelihood = MultivariateGaussian.log_likelihood(mu, sigma,
                                                                 X)
            res.append((f1, f3, log_likelihood))
    df = pd.DataFrame(res, columns=['f1', 'f3', 'Log Likelihood'])
    plotly.express.density_heatmap(df, x='f3', y='f1', z='Log Likelihood',
                                   title='Log Likelihood by Values of f1, f3',
                                   histfunc='avg').show()

    # Question 6 - Maximum likelihood
    row = df.iloc[df['Log Likelihood'].idxmax()]
    print(f"f1: {row['f1'].round(3)}, f3: {row['f3'].round(3)},"
          f" Log Likelihood: {row['Log Likelihood'].round(3)}")

if __name__ == '__main__':
    np.random.seed(0)
    test_univariate_gaussian()
    test_multivariate_gaussian()


import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits import mplot3d

def f(x):
    return x*(x - 2)*np.exp(3 - x)

x = np.linspace(-0.5, 3.0)  # 100 values between -0.5 and 3.0

y = f(x)                    # evaluate f on the x points => [f(x)[0-100]]

plt.plot(x, y)               # plot data

# plt.figure()                          # manually create a new figure
# plt.plot(x, f(x), x, x**2, x, 1 - x)  # muilty lines

# -------
# fig, ax = plt.subplots()  # subplot
# l1 = ax.plot(x, f(x))
# l2 = ax.plot(x, x**2)
# l3 = ax.plot(x, 1 - x)

# y1 = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
# y2 = np.array([1.2, 1.6, 3.1, 4.2, 4.8])
# y3 = np.array([3.2, 1.1, 2.0, 4.9, 2.5])

# fig, ax = plt.subplots()  # subplot
# lines = ax.plot(y1, 'o', y2, 'x', y3, '*')  # add styles line

# ax.set_title("Plot of the data y1, y2, and y3")
# ax.set_xlabel("x axis label")
# ax.set_ylabel("y axis label")

# ax.axis([-0.5, 5.5, 0, 5.5])  # set axes
# ax.set_xticks([0.5*i for i in range(9)])  # set xticks
# ax.set_yticks([0.5*i for i in range(11)])  # set yticks
# ax.grid()

# # add a legend follow order of plot(1, 2, 3)
# ax.legend(("data y1", "data y2", "data y3"))

# ------ seprate figure -----------------------------------------------------------------
# def generate_newton_iters(x0, number):
#     iterates = [x0]
#     errors = [abs(x0 - 1.)]
#     for _ in range(number):
#         x0 = x0 - (x0*x0 - 1.)/(2*x0)
#         iterates.append(x0)
#         errors.append(abs(x0 - 1.))
#     return iterates, errors

# iterates, errors = generate_newton_iters(2.0, 5)

# # print(iterates, errors)

# fig, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True)  # 1 row, 2 columns

# ax1.plot(iterates, "x")
# ax1.set_title("Iterates")
# ax1.set_xlabel("$i$")
# ax1.set_ylabel("$x_i$")

# ax2.semilogy(errors, "x")  # plot y on logarithmic scale
# ax2.set_title("Error")
# ax2.set_xlabel("$i$")
# ax2.set_ylabel("Error")
# ---------------------------------------------------------------------------------------

# -------------- Surface and contour plots
# X = np.linspace(-2, 2)
# Y = np.linspace(-1, 1)

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

# z = x**2 * y**3

# fig = plt.figure()
# ax = fig.add_subplot(projection="3d")  # declare 3d plot

# surface
# ax.plot_surface(x, y, z)

# ax.set_xlabel("$x$")
# ax.set_ylabel("$y$")
# ax.set_zlabel("$z$")

# ax.set_title("Graph of the function $f(x) = x^2y^3$")

# coutour
# plt.contour(x, y, z)
# plt.title("Contours of $f(x) = x^2y^3$")
# plt.xlabel("$x$")
# plt.ylabel("$y$")

# x = np.array([0.19, -0.82, 0.8, 0.95, 0.46, 0.71,
#               -0.86, -0.55, 0.75, -0.98, 0.55, -0.17, -0.89,
#               -0.4, 0.48, -0.09, 1., -0.03, -0.87, -0.43])
# y = np.array([-0.25, -0.71, -0.88, 0.55, -0.88, 0.23,
#               0.18, -0.06, 0.95, 0.04, -0.59, -0.21, 0.14, 0.94,
#               0.51, 0.47, 0.79, 0.33, -0.85, 0.19])
# z = np.array([-0.04, 0.44, -0.53, 0.4, -0.31, 0.13,
#               -0.12, 0.03, 0.53, -0.03, -0.25, 0.03, -0.1,
#               -0.29, 0.19, -0.03, 0.58, -0.01, 0.55, -0.06])

# # ------- trisurf
# fig = plt.figure(tight_layout=True)  # force new figure
# ax1 = fig.add_subplot(1, 2, 1, projection="3d")  # 3d axes
# ax1.plot_trisurf(x, y, z)
# ax1.set_xlabel("$x$")
# ax1.set_ylabel("$y$")
# ax1.set_zlabel("$z$")
# ax1.set_title("Approximate surface")

# ax2 = fig.add_subplot(1, 2, 2)  # 2d axes
# ax2.tricontour(x, y, z)
# ax2.set_xlabel("$x$")
# ax2.set_ylabel("$y$")
# ax2.set_title("Approximate contours")

# ------------- Customizing three-dimensional plots
# X = np.linspace(-2, 2)
# Y = np.linspace(-2, 2)
# x, y = np.meshgrid(X, Y)
# t = x**2 + y**2  # small efficiency
# z = np.cos(2*np.pi*t)*np.exp(-t)

# fig = plt.figure()
# ax = fig.add_subplot(projection="3d")
# ax.plot_surface(x, y, z, cmap="binary_r")

# ax.set_title("Surface with colormap")
# ax.set_xlabel("$x$")
# ax.set_ylabel("$y$")
# ax.set_zlabel("$z$")

# fig = plt.figure()
# plt.contour(x, y, z, cmap="binary_r")
# plt.xlabel("$x$")
# plt.ylabel("$y$")
# plt.title("Contour plot with colormap set")

plt.show()


#! /usr/bin/env python

# PyTorch Tutorial 05 - Gradient Descent with Autograd and Backpropagation
# https://www.youtube.com/watch?v=E-I2DNVzQLg&list=PLqnslRFeH2UrcDBWF5mfPGpqQDSta6VK4&index=6

#from __future__ import print_function
import numpy as np

print("\n" * 20)
print("-" * 80)
print("-" * 80)
print("\n" * 2)

# 0m - Manual, Prediction, Gradient Computation, Loss Computation, Parameter updates
# 12m10 - switch over from numpy to torch

# f = 2 * x
X = np.array([1,2,3,4], dtype=np.float32)
Y = np.array([2,4,6,8], dtype=np.float32)
w = 0.0       # weights

# model predition
def forward(x):
  return w * x

# loss = MSE - Mean Squared Error
def loss(y, y_predicted):
  return ((y_predicted - y)**2).mean()

# gradient - manually computed gradient
#       mean   prediction   actual value   squared
# MSE = 1/N *  (w*x       -    y)           **2
# J - objective function
# dJ/dw = 1/N 2*x (w*x - y)
def gradient(x, y, y_predicted):
  return np.dot(2*x, y_predicted-y).mean()

print(f'prediction BEFORE training: f(5) = {forward(5):.3f}')

learning_rate = 0.01
n_iters = 20 # 10

for epoch in range(n_iters):
  # prediction = forward pass
  y_pred = forward(X)

  # loss
  l = loss(Y, y_pred)

  # gradients
  dw = gradient(X, Y, y_pred)

  # update weights   -= negative gradient direction
  w -= learning_rate * dw

  if epoch % 1 ==  0:
    print(f'epoch {epoch+1}: w = {w:.3f}, loss = {l:.8f}')

print(f'prediction AFTER training: f(5) = {forward(5):.3f}')

#print(f"\n  \n{  }")

#print(f"\n  \n{  }")
print('\n')


# An example of OpenOpt EIG, see http://openopt.org/EIG for more examples and details
from openopt import EIG

# create a 5 x 5 matrix
import numpy.random as nr
nr.seed(0)
N = 5
A = nr.rand(N, N)

#define prob
p = EIG(A)
#solve
r = p.solve('numpy_eig') # solver numpy.linalg.eig will be used

print(r.eigenvalues) # [ 2.89776724+0.j  -0.65372843+0.j 0.14607289+0.19602952j  0.14607289-0.19602952j -0.08530815+0.j]
# for i-th eigenvalue r.eigenvectors[:,i]  is corresponding vector,
# as well as it is done for numpy/scipy functions
print(r.eigenvectors)
'''
[[ 0.43733688+0.j         -0.19592536+0.j          0.57285154+0.j
   0.57285154+0.j          0.63764724+0.j        ]
 [ 0.49662623+0.j          0.03219327+0.j         -0.14013112+0.23938241j
  -0.14013112-0.23938241j -0.53642409+0.j        ]
 [ 0.42977207+0.j          0.55544796+0.j         -0.17419089+0.24907549j
  -0.17419089-0.24907549j  0.29171743+0.j        ]
 [ 0.38727512+0.j         -0.62338178+0.j         -0.42011495-0.27666898j
  -0.42011495+0.27666898j -0.45403266+0.j        ]
 [ 0.47687818+0.j          0.51327338+0.j          0.48015310-0.13758665j
   0.48015310+0.13758665j  0.12004364+0.j        ]]
 '''


## gaussian.py
# emcee model file to estimate the mean and standard deviation of a gaussian distribution

# imports
from math import log, pi
import numpy as np

# define the natural logarithm of the likelihood
def ln_likelihood(θ, value):
    N = len(value)
    mu, sigma = θ

    sum = 0
    for i in range(0, N):
        sum += (-1/2) * ((value[i] - mu) / sigma)**2

    sum += -N*log(sigma) - (1/2)*log(2*pi)

    return sum

# define the natural logarithm of the priors
def ln_prior(θ):
    mu, sigma = θ

    # flat priors
    if -10 < mu < 10 and 1 < sigma < 10:
        return 0.0

    return -np.inf

# define the probability using the prior and likelihood
def ln_probability(θ, value):
    prior = ln_prior(θ)
    if not np.isfinite(prior):
        return -np.inf
    return prior + ln_likelihood(θ, value)


"""

The prime factors of 13195 are 5, 7, 13 and 29.

What is the largest prime factor of the number 600851475143 ?

"""

import sympy

def largest_prime_factor(n):
	prime_nos = []
	for i in range(2, int(n/2)):
		if n % i == 0 and sympy.isprime(i):
			prime_nos.append(i)
			print (prime_nos)

	return prime_nos

print (max(largest_prime_factor(600851475143)))


#Power spectrum Estimation of discrete sequence
#x[n] = (0.9)^n, 0<=n<20
import numpy as np
from numpy.fft import fft,ifft
import scipy as sy
from matplotlib import pyplot as plt
N = 16 #16 point dft
n = np.arange(0,20,1)
x1 = (0.9**n) #given discrete sequence
x = x1[0:15]
X = fft(x,N)
X_mag = abs(X)
Pxx = ((abs(X))**2)/N
K = np.arange(0,len(Pxx),1)
plt.figure(1)
plt.stem(K,Pxx,'grey')
plt.xlabel('Discrete Frequency Variable K---->')
plt.ylabel('Pxx-Periodogram---->')
plt.grid(True, which='both')
plt.title('Power Spectrum Estimation of Discrete Sequence')
plt.figure(2)
plt.stem(K,X_mag,'red')
plt.xlabel('Discrete Frequency Variable K---->')
plt.ylabel('Magnitude Response |X(K)|---->')
plt.grid(True, which='both')
plt.title('DFT of Discrete Sequence')
plt.show()


import numpy as np

def bisection(f, a, b, epsilon=10**-5):
    assert f(a)*f(b) <= 0
    assert epsilon > 0

    range_hist = [(a, b)]
    while not np.abs(b-a) <= epsilon:
        c = (a + b) / 2
        fc = f(c)
        if fc * f(a) > 0:
            a = c
        else:
            b = c
        range_hist.append((a, b))

    c = (a + b) / 2
    hist = np.asarray(range_hist)
    return c, hist


#!/home/knielbo/virtenvs/cv/bin/python
"""
Examples of approaches to weight initialization
"""
import numpy as np
def main():
    m = 64
    n = 32
    # constant initialization
    W = np.zeros((m,n))
    W = np.ones((m,n))
    C = 2
    W = W * C
    # uniform and normal dist initialization
    W = np.random.uniform(low=-.05, high=.05, size=(m,n))
    W = np.random.normal(.0, .05, size=(m,n))
    # LeCun uniform and normal
        # parameter F_{in} "fan in" or number of inputs to the layer
        #           F_{out} "fan out" or number og outputs from the layer
    F_in = 64
    F_out = 32
    limit = np.sqrt(3 / float(F_in))
        # uniform
    W = np.random.uniform(low=-limit, high=limit, size=(F_in, F_out))
        # normal
    limit = np.sqrt(1 / float(F_in))
    W = np.random.normal(.0, limit, size=(F_in, F_out))
    # Glorot/Xavier uniform and normal
        # normal
    limit = np.sqrt(2 / float(F_in + F_out))
    W = np.random.normal(.0, limit, size=(F_in, F_out))
        # uniform
    limit = np.sqrt(6 / float(F_in + F_out))
    W = np.random.uniform(low=-limit, high=limit, size=(F_in, F_out))
    # He et al./Kaiming/MSRA uniform and normal, for very deep models with ReLU-like
        # activation function
        # uniform
    limit = np.sqrt(6 / float(F_in))
    W = np.random.uniform(low=-limit, high=limit, size=(F_in, F_out))
        # normal
    limit = np.sqrt(2 / float(F_in))
    W = np.random.normal(.0, limit, size=(F_in, F_out))

    print(W)

if __name__=="__main__":
    main()


import sympy as sp, math as m

x = sp.symbols('x')
sp.init_printing(use_unicode=True)

#f=sp.Lambda(x, x*sp.cos(x)-x**2*sp.sin(x))
#f=sp.Lambda(x, sp.exp(sp.sin(x)**2-2))

"""
def f(x):
    #return x*m.cos(x)-x**2*m.sin(x)
    return m.exp(3*x)*m.sin(3*x)/(x**4+1)
"""

def trapecio_compuesta(f, a, b, n):

    h = (b-a)/n

    result = sp.N(f(a)+f(b))/2

    for i in range(1, n):
        result += sp.N(f(a+i*h))

    result *= h

    return result

"""
a, b = 0,2
n = 8

result = trapecio_compuesta(f, a, b, n)

print(result)

# Error

h = (b-a)/n

cota_2 = 0.73576

print("Cota error: "+str(abs((b-a)*h**2/12*cota_2)))
"""

# 2 Examen

f=sp.Lambda(x, (sp.cos(sp.pi*x)+1)**(10/3))
a, b = 0, 1
solution = 3.000492123714049

n = 2

for _ in range(6):
    result = trapecio_compuesta(f, a, b, n)
    print(str(n)+ " subintervalos:")
    print("\t"+str(result))
    print("\tError:" + str(abs(result-solution)))
    #print("\tError/4:" + str(abs(result-solution)/4))
    n*=2


## NUMERICAL INTEGRATION - FUNCTIONS FILE
#### Based on excercise from file *1_aproximacion_a_derivadas_e_integrales*
#### Author: Rob (GitHub: Roberto919)
#### Date: 19 June 2020

'--------------------------------------------------------------------------------'
#############
## Imports ##
#############

## Python libraries
import numpy as np
import math

'--------------------------------------------------------------------------------'
###############
## Functions ##
###############

## Tcf: composite trapezoidal method for f
def Tcf(f, a, b, n):
    """
    Compute numerical approximation using trapezoidal method in
    an interval.
    Nodes are generated via formula: x_i = a+i*h_hat for i=0,1,...,n and h_hat=(b-a)/n; h = (b - a)
        args:
            f (function): function expression of integrand
            a (int): left point of interval
            b (int): right point of interval
            n (int): number of subintervals
        returns:
            sum_res (float): numerical approximation to integral of f in the interval a,b
    """

    ## Constants
    h = (b - a)
    h_hat = (b - a)/n
    x_0 = 0
    x_n = math.pi

    ## Finding sum of x_i
    sum_fxi = 0
    for i in np.linspace(1, (n - 1), (n - 1)):

        x_i = a + i*h_hat

        sum_fxi += f(x_i)

    ## Estimating integral value
    approx_area_trapeze = h/(2*n)*(f(x_0) + f(x_n) + 2*sum_fxi)

    return approx_area_trapeze

# GLf: Gauss-Legendre quadrature for f
def GLf(f, a, b, n):
    """
    Compute numerical approximation using quadrature Gauss-Legendre.
    Weights and nodes are obtained with table for n=0,1,2,3,4
    Args:
        f (function): function expression of integrand
        a (int): left point of interval
        b (int): right point of interval
        n (int): number of subintervals
    Returns:
        sum_res (float): numerical approximation to integral of f in the interval a,b
    """

    ## List of calculated weights and node values (n = keys)
    wn_vals = {

        0: {
            'grade': 1,
            'vars': 2,
            'w': [2],
            'n': [0]
        },

        1: {
            'grade': 3,
            'vars': 4,
            'w': [1, 1],
            'n': [-np.sqrt(1/3), np.sqrt(1/3)]
        },

        2: {
            'grade': 5,
            'vars': 6,
            'w': [5/9, 8/9, 5/9],
            'n': [-np.sqrt(3/5), 0, np.sqrt(3/5)]
        },

        3: {
            'grade': 7,
            'vars': 8,
            'w': [0.347855, 0.652145, 0.652145, 0.347855],
            'n': [-0.861136, -0.339981, 0.339981, 0.861136]
        },

        4: {
            'grade': 9,
            'vars': 10,
            'w': [0.236927, 0.478629, 0.568889, 0.478629, 0.236927],
            'n': [-0.90618, -0.538469, 0, 0.538469, 0.90618]
        },
    }

    ## Approximation of the sum term
    sum_term = 0
    vals = len(wn_vals[n]['w'])
    for v in range(0, vals):
        sum_term += wn_vals[n]['w'][v]*f(0.5*((b - a)*wn_vals[n]['n'][v] + a + b))

    ## Obtaining final approximation result
    sum_res = (b - a)/2*sum_term

    return sum_res

## GHf: Gauss-Hermite quadrature for f
def GHf(f, mu, sigma):
    """
    Compute numerical approximation using quadrature Gauss-Hermite.
    Weights and nodes are obtained with table in Kiusalaas for n=6
        args:
            f (function): function expression of integrand
            mu (float): mean
            sigma (float): standard deviation
        returns:
            sum_res (float): numerical approximation to integral of f in the interval a,b
    """

    ## Default "n" value
    n = 5

    ## Nodes and weights for numerical approximation
    gausshermite_values = { ## Dict keys correspond to the value of "n"
        1: {
            'A_xs': {
                0.886227: [0.707107, -0.707107],
            }
        },
        2: {
            'A_xs': {
                1.181636: [0, -0],
                0.295409: [1.224745, -1.224745],
            }
        },
        3: {
            'A_xs': {
                0.804914: [0.524648, -0.524648],
                0.813128e-1: [1.650680, -1.650680],
            }
        },
        4: {
            'A_xs': {
                0.945308: [0, -0],
                0.393619: [0.958572, -0.958572],
                0.199532e-1: [2.020183, -2.020183],
            }
        },
        5: {
            'A_xs': {
                0.724629: [0.436077, -0.436077],
                0.157067: [1.335849, -1.335849],
                0.453001e-2: [2.350605, -2.350605]
            }
        },
    }

    ## Calculation of area approximate
    sum_res = 0
    for weight in gausshermite_values[n]['A_xs']:
        for node in gausshermite_values[n]['A_xs'][weight]:
            sum_res += weight*f(node)

    return sum_res


# Question: https://projecteuler.net/problem=108

"""
1/x + 1/y = 1/n
Since n > 0, we have x > n and y > n. Assume x >= y.
Let x = n + a, y = n + b.
   1/x + 1/y = 1/n
-> xn + yn = xy
-> (n+a)n + (n+b)n = (n+a)(n+b)
-> 2n^2 + an + bn = n^2 + an + bn + ab
-> n^2 = ab
The number of solutions of (x,y) for each n is the number of solutions of (a,b) for each n.
The number of positive integers (a,b) where a >= b that ab = n^2 is (m-1)/2+1, where m is the number of divisors of n^2.
Why?
    | Let the number of divisors be 'm'.
    | For each divisor d where d != n, let (a, b) = (d, n^2/d).
    |   Note that, (d, n^2/d) is the same as (n^2/d, d), where n^2/d is also a divisor but only one of them have a > b.
    |   There are (m-1) such a case, therefore there are (m-1)/2 pair of (a,b) that a > b.
    | For the case (a, b) = (n, n); there is only one such a case.
    | So, in total, there are (m-1)/2+1 pairs of (a,b).

In this problem, we want to find the smallest n such that (number_of_divisors of n**2) >= 1999.
"""

from sage.all import number_of_divisors

ans = 0
while True:
    ans = ans + 1
    if number_of_divisors(ans**2) >= 1999:
        break
print(ans)


import numpy as np
from tabulate import tabulate
import sympy
import scipy.integrate as integrate
from mpmath import chebyt, chop, taylor

x = sympy.symbols('x')

def printMatrix(A, B):
    selected = None
    for row in range(len(B)):
        print("(", end='')
        for col in range(len(A[row])):
            print("\t{1:10.2f}{0}".format(" " if (selected is None
                                                  or selected != (row, col)) else "*", A[row][col]), end='')
        print("\t) * (\tX{0}) = (\t{1:10.2f})".format(row + 1, B[row][0]))

# норма матрицы, где p = infinity
def infnorm(A):
    return max(map(max, A))

# скалярное произведение (Lwj, wi)
def scalar(i, j):
    x = sympy.symbols('x')
    wj = sympy.jacobi(j, 1, 1, x) * (1 - np.power(x, 2))
    d = sympy.lambdify(x, sympy.diff(wj, x, 1))
    d2 = sympy.lambdify(x, sympy.diff(wj, x, 2))
    l = integrate.quad(lambda x: (-1 * d2(x) / (x - 3) + (1 + x / 2) * d(x)
                                 + sympy.exp(x /2) * sympy.jacobi(j, 1, 1, x) * (1 - np.power(x, 2))) * sympy.jacobi(i, 1, 1, x) * (1 - np.power(x, 2)), -1, 1)[0]
    return l

def Lu(wj):
    x = sympy.symbols('x')
    d = sympy.lambdify(x, sympy.diff(wj, x, 1))
    d2 = sympy.lambdify(x, sympy.diff(wj, x, 2))
    h = sympy.diff(sympy.diff(wj, x, 1))
    return -1 * d2(x) / (x - 3) + (1 + x / 2) * d(x) + sympy.exp(x / 2) * wj

def f(x):
    return 2 - x

def solution(x0, C): # формируем решение
    result = 0
    for i in range(len(C)):
        result += C[i] * sympy.lambdify(x, w[i])(x0)
    return result

# печать результата для нескольких n
def printresult(Cond):
    headers = ["n", "mu(A)", "y^n(-0.5)", "y^n(0)", "y^n(0.5)", "y*(x) - y^n(x)"]
    print(tabulate(Cond, headers, tablefmt='grid'))

# печать результата для одного n
def printSolon(A, B, mu, C, n):
    print("Расширенная матрица:")
    printMatrix(A, B)

    print("Число обусловленности матрицы A:")
    print(mu)

    print("Коэффициенты разложения С:")
    print(C)

# метод Галеркина
def galerkin(n, w):
    A = np.eye(n)
    B = np.ones((n, 1))

    for i in range(n):
        B[i] = integrate.quad(lambda x: (2 - x) * sympy.jacobi(i, 1, 1, x) * (1 - np.power(x, 2)), -1, 1)[0]
        for j in range(n):
            A[i][j] = scalar(i, j)
    C = np.linalg.solve(A, B)
    mu = infnorm(A) * infnorm(np.linalg.inv(A))
    return A, B, C, mu

# Метод коллокации
def collocation(n, w):
    t = sorted(
        np.squeeze(np.asarray(np.roots(chop(taylor(lambda x: chebyt(n, x), 0, n))[::-1]))))  # Корни многочлена Чебышева

    A = np.eye(n)
    B = np.ones((n, 1))
    for i in range(n):
        B[i] = f(t[i])
        for j in range(n):
            A[i][j] = sympy.lambdify(x, Lu(w[j]))(t[i])
    C = np.linalg.solve(A, B)
    mu = infnorm(A) * infnorm(np.linalg.inv(A))
    return A, B, C, mu

print("Проекционные методы решения краевой задачи для обыкновенного дифференциального уравнения второго порядка")
print("Вариант 3")
v = int(input("Введите число координатных функций или нажмите 0, чтобы оставить значения от 3 до 10:"))

CondGalerkin = []
CondColloc = []
if v == 0:
    for n in range(3, 11):
        w = [] # формируем семейство ортогональных функций — здесь это многочлены Якоби
        for i in range(n):
            w.append(sympy.jacobi(i, 1, 1, x) * (1 - np.power(x, 2)))

        A, B, C, mu = galerkin(n, w)
        A1, B1, C1, mu1 = collocation(n, w)

        CondGalerkin.append([n, mu, solution(-0.5, C), solution(0, C), solution(0.5, C), np.abs(solution(-0.5, C) - solution(-0.5, C1))])
        CondColloc.append([n, mu1, solution(-0.5, C1), solution(0, C1), solution(0.5, C1), np.abs(solution(-0.5, C) - solution(-0.5, C1))])

    print("Метод Галёркина:")
    printresult(CondGalerkin)

    print("Метод коллокации:")
    printresult(CondColloc)
else:
    n = v
    w = []
    for i in range(n):
        w.append(sympy.jacobi(i, 1, 1, x) * (1 - np.power(x, 2)))

    A, B, C, mu = galerkin(n, w)
    A1, B1, C1, mu1 = collocation(n, w)

    print("Метод Галёркина:")
    printSolon(A, B, mu, C, n)

    print("Метод коллокации:")
    printSolon(A1, B1, mu1, C1, n)

import numpy,import scipy,import sympy,from mpmath