"""
This folder contains helper functions for common mathematical functions.
Note: this package is not equivalent to PyTorch's functional API, because
no backwards() methods are provided for these functions!

To use these functions as layers, please use the implementation in
layers/ or losses/ instead.
"""
import numpy as np

from edutorch.typing import NPArray

def softmax(f: NPArray) -> NPArray:
    """Numerically stable implementation of softmax."""
    f -= np.max(f)
    exp_f = np.exp(f)
    if len(f.shape) == 1:
        return exp_f / np.sum(exp_f, axis=0)
    return exp_f / np.sum(exp_f, axis=1).reshape(-1, 1)

def sigmoid(x: NPArray) -> NPArray:
    """A numerically stable version of the logistic sigmoid function."""
    pos_mask = x >= 0
    neg_mask = x < 0
    z = np.zeros_like(x)
    z[pos_mask] = np.exp(-x[pos_mask])
    z[neg_mask] = np.exp(x[neg_mask])
    top = np.ones_like(x)
    top[neg_mask] = z[neg_mask]
    return top / (1 + z)

__all__ = ("softmax", "sigmoid")


#Python code for chapter 17 DSILT: Statistics

import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder, MinMaxScaler, StandardScaler
from sklearn.manifold import MDS, Isomap, LocallyLinearEmbedding, TSNE
import matplotlib.pyplot as plt

#Set seed for repeatability
seed = 14
np.random.seed(seed)

#Load Iris data and specify dependent variable
alldata = pd.read_csv('/home/dsilt/Desktop/dsilt-stats-code/17-20 and 22 Iris and Motor Trends Cars/iris.csv')
print(alldata.info())
print(alldata.head())

#Split data into x and y, and format as numpy arrays
array = alldata.values
x = array[:, 0:4].astype(float)
y = array[:, 4]
#Convert the classes (species) into integers
encoder = LabelEncoder()
encoder.fit(y)
y = encoder.transform(y)

#Scale every feature using min-max normalization and z-score standardization
#Note that y is not scaled because it is categorical, but if it weren't, it would have to be scaled too
x_norm = MinMaxScaler().fit_transform(x)
x_std = StandardScaler().fit_transform(x)

'''
-------------------------------------------------------------------------------
-------------------------Multi-Dimensional Scaling-----------------------------
-------------------------------------------------------------------------------
'''

#Apply metric MDS, keeping n components < the number of original features
#kernel choices: linear (default), poly (polynomial of degree=degree), rbf, sigmoid, cosine
mds_model = MDS(n_components=2, metric=True, random_state=seed)
mds_model.fit_transform(x_std)
print(mds_model.get_params())
mds_dim = mds_model.embedding_
print(mds_dim.shape)  #There should be 2 latent variables represented
print('Stress:', mds_model.stress_)

#Plot first 2 extracted features and the observation class
plt.figure(figsize=(10, 5))
plt.xlabel('Latent Variable 1')
plt.ylabel('Latent Variable 2')
plt.title('Metric MDS 2-Dimension Plot with Observation Class')
plt.scatter(mds_dim[:, 0], mds_dim[:, 1], c=y)
plt.colorbar()
plt.show()

#Apply non-metric MDS, keeping n components < the number of original features
#kernel choices: linear (default), poly (polynomial of degree=degree), rbf, sigmoid, cosine
mds_model = MDS(n_components=2, metric=False, random_state=seed)
mds_model.fit_transform(x_std)
print(mds_model.get_params())
mds_dim = mds_model.embedding_
print(mds_dim.shape)  #There should be 2 latent variables represented
print('Stress:', mds_model.stress_)

#Plot first 2 extracted features and the observation class
plt.figure(figsize=(10, 5))
plt.xlabel('Latent Variable 1')
plt.ylabel('Latent Variable 2')
plt.title('Nonmetric MDS 2-Dimension Plot with Observation Class')
plt.scatter(mds_dim[:, 0], mds_dim[:, 1], c=y)
plt.colorbar()
plt.show()

#Apply metric MDS for many different choices of dimensions
#Limitation is that nbr dimensions must be < the number of original features
nbr_dim = range(3)
mds_dim_nbr = []
mds_stress_results = []
for nd in nbr_dim:
    mds_model = MDS(n_components=nd+1, metric=True, random_state=seed)
    mds_model.fit_transform(x_std)
    mds_dim_nbr.append(nd+1)
    mds_stress_results.append(mds_model.stress_)
mds_results = {'nbr_dim': mds_dim_nbr, 'stress': mds_stress_results}
mds_results = pd.DataFrame.from_dict(mds_results)
#See which number of dimensions has the lowest stress
plt.plot(mds_results['nbr_dim'], mds_results['stress'])
plt.xlabel('Number of Latent Dimensions')
plt.ylabel('Stress')
plt.title('Scree Plot of Stress by Number of Latent Variables')
plt.show()

#Build MDS model with one less than the number of dimensions at the kneee of the scree plot
#In this case, that would be 1, so the index is substituted for the second latent variable
mds_model = MDS(n_components=1, metric=True, random_state=seed)
mds_model.fit_transform(x_std)
mds_dim = mds_model.embedding_
plt.figure(figsize=(10, 5))
plt.xlabel('Index')
plt.ylabel('Latent Variable 1')
plt.title('Metric MDS 1-Dimension Plot with Observation Class')
plt.scatter(mds_dim[:, 0], list(alldata.index), c=y)
plt.colorbar()
plt.show()

'''
-------------------------------------------------------------------------------
-----------------------------------Isomap--------------------------------------
-------------------------------------------------------------------------------
'''

#Apply isomap embedding, keeping n components < the number of original features
iso_model = Isomap(n_neighbors=5, n_components=2)
iso_model.fit_transform(x_std)
print(iso_model.get_params())
iso_dim = iso_model.embedding_
print(iso_dim.shape)  #There should be 2 latent variables represented

#Plot first 2 extracted features and the observation class
plt.figure(figsize=(10, 5))
plt.xlabel('Latent Variable 1 (explains most variance)')
plt.ylabel('Latent Variable 2 (explains second most variance)')
plt.title('Isomap 2-Dimension Plot with Observation Class')
plt.scatter(iso_dim[:, 0], iso_dim[:, 1], c=y)
plt.colorbar()
plt.show()

#Apply isomap for many different choices of dimensions
#Limitation is that nbr dimensions must be < the number of original features
nbr_dim = range(3)
iso_dim_nbr = []
iso_reconstruction_errors = []
for nd in nbr_dim:
    iso_model = Isomap(n_neighbors=5, n_components=nd+1)
    iso_model.fit_transform(x_std)
    iso_dim_nbr.append(nd+1)
    iso_reconstruction_errors.append(iso_model.reconstruction_error())
iso_results = {'nbr_dim': iso_dim_nbr, 'error': iso_reconstruction_errors}
iso_results = pd.DataFrame.from_dict(iso_results)
#See which number of dimensions has the lowest reconstruction error
plt.plot(iso_results['nbr_dim'], iso_results['error'])
plt.xlabel('Number of Latent Dimensions')
plt.ylabel('Reconstruction Error')
plt.title('Plot of Error by Number of Latent Variables')
plt.show()

#Use iso_model.transform(x_test) to fit the isomap from the training set onto the test set

'''
-------------------------------------------------------------------------------
-------------------------------Modified LLE------------------------------------
-------------------------------------------------------------------------------
'''

#Apply modified LLE, keeping n components < the number of original features
#method = 'standard' for LLE, 'hessian' for HELLE, or 'modified' for modified LLE
mlle_model = LocallyLinearEmbedding(n_neighbors=5, n_components=2, method='modified', random_state=seed)
mlle_model.fit_transform(x_std)
print(mlle_model.get_params())
mlle_dim = mlle_model.embedding_
print(mlle_dim.shape)  #There should be 2 latent variables represented

#Plot first 2 extracted features and the observation class
plt.figure(figsize=(10, 5))
plt.xlabel('Latent Variable 1 (explains most variance)')
plt.ylabel('Latent Variable 2 (explains second most variance)')
plt.title('Modified LLE 2-Dimension Plot with Observation Class, 5 neighbors')
plt.scatter(mlle_dim[:, 0], mlle_dim[:, 1], c=y)
plt.colorbar()
plt.show()

#Try a different number of neighbors
mlle_model = LocallyLinearEmbedding(n_neighbors=15, n_components=2, method='modified', random_state=seed)
mlle_model.fit_transform(x_std)
mlle_dim = mlle_model.embedding_
plt.figure(figsize=(10, 5))
plt.xlabel('Latent Variable 1 (explains most variance)')
plt.ylabel('Latent Variable 2 (explains second most variance)')
plt.title('Modified LLE 2-Dimension Plot with Observation Class, 15 neighbors')
plt.scatter(mlle_dim[:, 0], mlle_dim[:, 1], c=y)
plt.colorbar()
plt.show()

#Use mlle_model.transform(x_test) to fit the modified LLE from the training set onto the test set

'''
-------------------------------------------------------------------------------
-------------------------------------t-SNE-------------------------------------
-------------------------------------------------------------------------------
'''

#Build TSNE model, learning rate defaults to 1000 but usually best around 200
#Perplexity balances local and global aspects of neighbors, usually best between 5 and 50
tsne_model = TSNE(n_components=2, perplexity=30.0, learning_rate=100.0,
                  n_iter=2000, n_iter_without_progress=30,
                  random_state=seed, method='barnes_hut')
tsne_model.fit_transform(x_std)
print(tsne_model.get_params())
tsne_dim = tsne_model.embedding_
print(tsne_dim.shape)  #There should be 2 latent variables represented
#print('Kullback-Leibler divergence:', tsne_model.kl_divergence_)

#Plot first 2 extracted features and the observation class
plt.figure(figsize=(10, 5))
plt.xlabel('Latent Variable 1')
plt.ylabel('Latent Variable 2')
plt.title('t-SNE 2-Dimension Plot with Observation Class \nperplexity=30, learning_rate=100')
plt.scatter(tsne_dim[:, 0], tsne_dim[:, 1], c=y)
plt.colorbar()
plt.show()

17-20 and 22 Iris and Motor Trends Cars/17_nonlinear_dimension_reduction.py


import numpy as np
import proj_gd as gd
import utils
import matplotlib.pyplot as plt
import matplotlib.patches as mp

X = np.array([[2, 0, 0], [0, 1, 0]]).T
y = np.array([3, 2, 2])
t = 1

def f(w):
    return 0.5*np.sum((X@w - y)**2)

def fp(w):
    return X.T@(X@w - y)

def proj_l1_ball(x, t):
    if np.linalg.norm(x, 1) <= t:
        return x
    x2 = np.sort(x)[::-1]
    y = np.cumsum(x2)
    n = len(x)
    for k in range(n):
        c = (y[k]-t) / (k + 1)
        if c > x2[k]:
            mu = (y[k-1]-t) / k
            break
    else:
        mu = (y[n-1]-t) / n
    return np.maximum(0, x - mu)

w0 = np.array([-1, 0.5])
stepsize = 0.1

w_traces, y_traces = gd.proj_gd(fp, lambda x: proj_l1_ball(
    x, t), w0, stepsize=stepsize, tol=1e-8)

f_value = f(w_traces[-1])

print()
print('t = ', t)
print('number of iterations:', len(w_traces)-1)
print('solution:', w_traces[-1])
print('value:', f_value)

# visualization
Q = X.T@X
b = X.T@y
c = y@y

def f_2d(w1, w2):
    return 0.5 * Q[0, 0] * w1**2 + 0.5 * Q[1, 1] * w2**2 + Q[0, 1] * w1 * w2 \
        - b[0] * w1 - b[1] * w2 - 0.5 * c

def gap(w):
    return f(w) - f_value

path = 'figures/'
feasible_set = mp.Polygon(
    [(-t, 0), (0, t), (t, 0), (0, -t)], alpha=0.5, color='y')
utils.plot_traces_2d(f_2d, w_traces, y_traces, feasible_set,
                     path+f'lasso_traces_t{t}.png')
utils.plot(gap, w_traces, path+f'lasso_gap_t{t}.png')


import numpy as np
import matplotlib as mpl
import matplotlib.cm as cm
import matplotlib.pyplot as plt

def logistic(z: np.ndarray, a: float, b: float) -> np.ndarray:
    """ Compute logistic function
      Inputs:
         a: exponential parameter
         b: exponential prefactor
         z: numpy array; domain
      Outputs:
         f: numpy array of floats, logistic function
    """

    den = 1.0 + b * np.exp(-a * z)
    return 1.0 / den

def stretch_tanh(z: np.ndarray, a: float, b: float, c: float) -> np.ndarray:
    """ Compute stretched hyperbolic tangent
      Inputs:
         a: horizontal stretch parameter (a>1 implies a horizontal squish)
         b: vertical stretch parameter
         c: vertical shift parameter
         z: numpy array; domain
      Outputs:
         g: numpy array of floats, stretched tanh
    """
    return b * np.tanh(a * z) + c

def relu(z: np.ndarray, eps: float = 0.01) -> np.ndarray:
    """ Compute rectificed linear unit
      Inputs:
         eps: small positive parameter
         z: numpy array; domain
      Outputs:
         h: numpy array; relu
    """
    return np.fmax(z, eps * z)

x = np.linspace(-5.0, 5.0, 100) # Equally spaced grid of 100 pts between -5 and 5

# First get the data
f = logistic(x, 2.0, 1.0)
g = stretch_tanh(x, 2.0, 0.5, 0.5)
h = relu(x)

fig, ax = plt.subplots(1,1, figsize=(10,6)) # Create figure object

# Make actual plots
# (Notice the label argument!)
ax.plot(x, f, lw=4, ls='-', label=r'$L(x;1)$')
ax.plot(x, g, lw=4, ls='--', label=r'$\tanh(2x)$')
ax.plot(x, h, lw=4, ls='-.', label=r'$relu(x; 0.01)$')

# Make the tick labels readable
ax.tick_params(labelsize=24)

# Set axes limits to make the scale nice
ax.set_xlim(x.min(), x.max())
ax.set_ylim(h.min(), 1.1)

# Make readable labels
ax.set_xlabel(r'$x$', fontsize=24)
ax.set_ylabel(r'$h(x)$', fontsize=24)
ax.set_title('Activation Functions', fontsize=24)

# Set up grid
ax.grid(True, lw=1.75, ls='--', alpha=0.75)

# Put legend on figure
ax.legend(loc='best', fontsize=24);

#fig.savefig('figs/nice_plots.png')


# Implementation of Naive Bayes(NB) classifier.
# Tested on MNIST dataset.

# Code Flow:
    # 1. Import all relevant libraries.
    # 2. NB class definition.
        # 2.1 Fit function.
        # 2.2 Predict function.
        # 2.3 Score function.
    # 3. Main Function.
        # 3.1 Read the MNIST dataset.
        # 3.2 Test train split.
        # 3.3 NB function call.

# 1. Import all relevant libraries:
import numpy as np
import pandas as pd
from datetime import datetime
from scipy.stats import multivariate_normal as mvn
from pathlib import Path

# 2. Naive Bayes class definition:
class NaiveBayes(object):
    # Variables: gaussians, priors
    # Methods: fit, predict, score

    # 2.1 Fit function:
    def fit(self, X, Y, smoothing=1e-2): # smoothing to avoid div by 0 for variance.
        self.gaussians = dict() # tuples (mean, variance)
        self.priors = dict()
        labels = set(Y)
        for c in labels: # loop through each labels.
            current_x = X[Y == c]
            self.gaussians[c] = {
                'mean': current_x.mean(axis=0),
                'var': current_x.var(axis=0) + smoothing,
            }
            self.priors[c] = float(len(Y[Y == c])) / len(Y)

    # 2.2 Predict function:
    def predict(self, X):
        N, D = X.shape
        K = len(self.gaussians)
        P = np.zeros((N, K))
        for c, g in (self.gaussians).items():
            mean, var = g['mean'], g['var']
            # Use scipy logpdf function because exp slows it down
            # Covariance can be specified as vector
            P[:,c] = mvn.logpdf(X, mean=mean, cov=var) + np.log(self.priors[c])
        return np.argmax(P, axis=1)

    # 2.3 Score function:
    def score(self, X, Y):
        P = self.predict(X)
        return np.mean(P == Y)

# 3. Main Function:
if __name__ == '__main__':

    # 3.1 Read the MNIST dataset:
    p = Path(__file__).parents[2]
    df = pd.read_csv(p /'0.Data' / 'train.csv')
    np.random.seed(101) # Set seed before shuffling
    data = df.as_matrix() # DF to matrix
    np.random.shuffle(data) # Shuflle data
    X = data[:, 1:] / 255.0 # Scale input data such that it is between [0,1] from [0,255]
    Y = data[:, 0]
    limit = 2000
    # Limit the number of data points to speed things up:
    if limit is not None:
        X, Y = X[:limit], Y[:limit]

    # 3.2 Test train split:
    Ntrain = len(Y) // 2
    Xtrain, Ytrain = X[:Ntrain], Y[:Ntrain]
    Xtest, Ytest = X[Ntrain:], Y[Ntrain:]

    # 3.3 NB function call:
    model = NaiveBayes() # Initilalize the model

    # Training:
    t0 = datetime.now()
    model.fit(Xtrain, Ytrain)
    print("Training time:", (datetime.now() - t0))

    # Prediction on training dataset:
    t0 = datetime.now()
    print("Train accuracy:", model.score(Xtrain, Ytrain))
    print("Time to compute train accuracy:", (datetime.now() - t0), "Train size:", len(Ytrain))

    # Prediction on test dataset:
    t0 = datetime.now()
    print("Test accuracy:", model.score(Xtest, Ytest))
    print("Time to compute test accuracy:", (datetime.now() - t0), "Test size:", len(Ytest))

4.Bayes Classifier/1.Code - Using Theory/0.Naive_Bayes.py


import numpy as np  # Matrix
from sklearn.datasets import make_regression  # Random dataset
import matplotlib.pyplot as plt  # Graphic

# Linear regression module
class Lr:
    # Hyper parameters
    learning_rate = 0.01
    n_iteration = 100
    n_samples = 100

    # Configuration
    n_param_more = 0
    use_normal_vector = False
    verbose = False
    name = ""

    # dataset
    x = None
    y = None
    X = None
    n_features = 1
    theta = None

    # result
    theta_final = None
    coef = 0

    def run(self, name=""):  # return the coef and the final theta
        if name == "":
            name = self.name

        self.y = self.y.reshape(self.y.shape[0], 1)

        self.X = self.make_X(self.n_features + self.n_param_more)  # X -> arguments input [[a, 1],[b, 1]...]

        if not self.use_normal_vector:

            if self.theta is None:
                self.theta = np.random.randn(self.X.shape[1], 1)  # Matrix with the arguments of the function

            # Learn
            self.theta_final, cost_history = self.gradient_descent()
        else:
            self.theta = self.normal_vector()
            self.theta_final = self.theta

        self.coef = self.coef_determination()  # Compute the coef / 1

        if self.verbose:
            print("\nStats: Linear regression ({})".format(name))  # Show Stats
            print("Use normal vector: " + str(self.use_normal_vector))
            print("Samples: " + str(self.n_samples))
            print("Features: " + str(self.n_features))
            print("More parameters: " + str(self.n_param_more))
            if not self.use_normal_vector:
                print("Iteration: " + str(self.n_iteration))
                print("Learning rate: " + str(self.learning_rate))
            print("y: " + str(self.y.shape))
            print("x: " + str(self.x.shape))
            print("X: " + str(self.X.shape))
            print("Theta: " + str(self.theta_final.shape))
            print("Theta final: " + str(self.theta_final))
            print("Coef: {0:9.3f}/1 ({0})".format(self.coef))

            # Show graphs
            for i in range(0, self.n_features):
                plt.scatter(self.x[:, i], self.y)
                plt.scatter(self.x[:, i], self.model(self.X), c='r')
                plt.show()

            #  Show cost evolution
            if not self.use_normal_vector:
                plt.plot(range(self.n_iteration), cost_history)
                plt.show()

        return self.theta_final, self.coef

    """=================================================================================================================
    ¦¦¦ Machine Learning FUNCTION                                                                                ¦¦¦
    ================================================================================================================="""

    def model(self, x):
        return x.dot(self.theta)

    def cost(self):
        m = len(self.y)
        return 1 / (2 * m) * np.sum((self.model(self.X) - self.y) ** 2)

    def grad(self):
        m = len(self.y)
        return 1 / m * self.X.T.dot(self.model(self.X) - self.y)

    def gradient_descent(self):
        if self.verbose:
            cost_history = np.zeros(self.n_iteration)

        for i in range(0, self.n_iteration):
            self.theta = self.theta - self.learning_rate * self.grad()
            if self.verbose:
                cost_history[i] = self.cost()
        return self.theta, cost_history

    def normal_vector(self):
        return np.linalg.inv(self.X.T.dot(self.X)).dot((self.X.T.dot(self.y)))

    def coef_determination(self):
        pred = self.model(self.X)
        u = ((self.y - pred) ** 2).sum()
        v = ((self.y - self.y.mean()) ** 2).sum()
        return 1 - u / v

    """=================================================================================================================
    ¦¦¦ CONSTRUCTION FUNCTION                                                                                ¦¦¦
    ================================================================================================================="""

    def make_X(self, n_param):
        if n_param <= 2:
            return np.hstack((self.x, np.ones((self.x.shape[0], 1))))
        else:
            return np.hstack((self.x ** (n_param - 1), self.make_X(n_param - 1)))

    def random_dataset(self, noise):
        self.x, self.y = make_regression(n_samples=self.n_samples, n_features=self.n_features,
                                         noise=noise)  # x -> inputs  y -> outputs
        self.y = self.y.reshape(self.y.shape[0], 1)
        return self.x, self.y


import numpy as np
import matplotlib.pyplot as plt
from psecas import Solver, FourierGrid, System

"""
The Mathieu equation is given by

    -uₓₓ + 2 q cos(2x) = σ u

where σ is the eigenvalue and q is a parameter.

We solve for u(x) and σ by using the Fourier grid.

This example is taken from the book *Spectral methods in Matlab*
by Lloyd Trefethen and this Python script reproduces the figure on
page 89.
"""

# Create grid
grid = FourierGrid(64, zmin=0, zmax=2*np.pi, z='x')

# Create the system
system = System(grid, variables='u', eigenvalue='sigma')
system.q = 1

# Add the first (and only) equation
system.add_equation("sigma*u = 2*q*np.cos(2*x)*u - dx(dx(u))")

# Create a solver object
solver = Solver(grid, system)

def sorting_strategy(E):
    """Sorting strategy. E is a list of eigenvalues"""
    # Sort from smallest to largest eigenvalue
    index = np.argsort(E)
    return (E, index)

solver.sorting_strategy = sorting_strategy

sigmas = []
for q in np.arange(0, 15, 0.2):

    # Set q in system object
    system.q = q

    # Solve
    solver.solve(saveall=True)

    # Append 11 lowest eigenvalues
    sigmas.append(solver.E[:11])

# Now plot the results
plt.figure(1)
plt.clf()
fig, axes = plt.subplots(num=1)
sigmas = np.array(sigmas)
for mode in range(11):
    axes.plot(np.arange(0, 15, 0.2), sigmas[:, mode].real)

axes.set_xlabel(r'$q$')
axes.set_ylabel(r'$\sigma$')
plt.show()


%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import scipy.fftpack

fig = plt.figure(figsize=[14,4])
N = 600           # Number of samplepoints
Fs = 800.0
T = 1.0 / Fs      # N_samps*T (#samples x sample period) is the sample spacing.
N_fft = 80        # Number of bins (chooses granularity)
x = np.linspace(0, N*T, N)     # the interval
y = np.sin(50.0 * 2.0*np.pi*x) + 0.5*np.sin(80.0 * 2.0*np.pi*x)   # the signal

# removing the mean of the signal
mean_removed = np.ones_like(y)*np.mean(y)
y = y - mean_removed

# Compute the fft.
yf = scipy.fftpack.fft(y,n=N_fft)
xf = np.arange(0,Fs,Fs/N_fft)

##### Plot the fft #####
ax = plt.subplot(121)
pt, = ax.plot(xf,np.abs(yf), lw=2.0, c='b')
p = plt.Rectangle((Fs/2, 0), Fs/2, ax.get_ylim()[1], facecolor="grey", fill=True, alpha=0.75, hatch="/", zorder=3)
ax.add_patch(p)
ax.set_xlim((ax.get_xlim()[0],Fs))
ax.set_title('FFT', fontsize= 16, fontweight="bold")
ax.set_ylabel('FFT magnitude (power)')
ax.set_xlabel('Frequency (Hz)')
plt.legend((p,), ('mirrowed',))
ax.grid()

##### Close up on the graph of fft#######
# This is the same histogram above, but truncated at the max frequence + an offset.
offset = 1    # just to help the visualization. Nothing important.
ax2 = fig.add_subplot(122)
ax2.plot(xf,np.abs(yf), lw=2.0, c='b')
ax2.set_xticks(xf)
ax2.set_xlim(-1,int(Fs/6)+offset)
ax2.set_title('FFT close-up', fontsize= 16, fontweight="bold")
ax2.set_ylabel('FFT magnitude (power) - log')
ax2.set_xlabel('Frequency (Hz)')
# ax2.hold(True)
ax2.grid()

plt.yscale('log')


"""
These Scene are made in 3Blue1Brown's Manim.
These Scenes will be considered as Externally Animated Scenes.
"""

from manimlib import *
from numpy import *

class KleinBottle(ThreeDScene):
    def construct(self):
        frame = self.camera.frame
        frame.reorient(-45, 70)
        frame.add_updater(
            lambda m, dt: m.increment_theta(2 * dt * DEGREES)
        )
        light = self.camera.light_source
        light.move_to([-10, -10, 20])
        self.add(frame)

        klein_bottle = ParametricSurface(
            lambda u, v : np.array([
                (-2/15)*cos(u)*( 3*cos(v) - 30*sin(u) + 90*(cos(u)**4)*sin(u) - 60*(cos(u)**6)*sin(u) + 5*cos(u)*cos(v)*sin(u) ),
                (-1/15)*sin(u)*( 3*cos(v) - 3*(cos(u)**2)*cos(v) - 48*(cos(u)**4)*cos(v) + 48*(cos(u)**6)*cos(v) - 60*sin(u) + 5*cos(u)*cos(v)*sin(u)
                - 5*(cos(u)**3)*cos(v)*sin(u) - 80*(cos(u)**5)*cos(v)*sin(u) + 80*(cos(u)**7)*cos(v)*sin(u) ),
                (2/15)*( 3 + 5*cos(u)*sin(u) )*sin(v)
                ]),
            u_range = [0, np.pi],
            v_range = [0, 2*np.pi]
        )
        klein_bottle.set_gloss(1.0)
        klein_bottle.set_opacity(0.5)
        klein_bottle.sort_faces_back_to_front(DOWN)
        mesh = SurfaceMesh(klein_bottle, resolution = (21, 11))
        mesh.set_stroke(BLUE, 1, 1)
        klein_bottle = Group(klein_bottle, mesh)
        klein_bottle.move_to(ORIGIN)
        klein_bottle.scale(2)
        self.add(klein_bottle)
        self.wait(2)


import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as sp
import random as rm
import math
import NumerosGenerados as ng
from Tests import testExpo

n = 100000
inicio = 0
alfa = 2
numeros_uniformes = sp.expon.rvs(size=n, loc = inicio, scale=1/alfa)

print("Media: ", round(np.mean(numeros_uniformes),3))
print("Desvio: ", round(np.sqrt(np.var(numeros_uniformes)),3))
print("Varianza: ", round(np.var(numeros_uniformes),3))

plt.hist(numeros_uniformes, bins=50, color='skyblue', histtype="bar",alpha=0.8,ec="black")
plt.xlabel("valores")
plt.ylabel("Frecuencia Absoluta")
plt.show()

#----------Naylor----------
randomGCL = ng.generarNumeros(n)

exponenciales=[]

def exponencial(alfa):
    EX= 1/alfa
    for r in randomGCL:
        x= -EX*math.log(r)
        exponenciales.append(x)
    print("Media: ", round(np.mean(exponenciales),3))
    print("Desvio: ", round(np.sqrt(np.var(exponenciales)),3))
    print("Varianza: ", round(np.var(exponenciales),3))
    plt.title("Distribuciones Exponenciales")
    plt.hist(numeros_uniformes, bins=50, color='skyblue', histtype="bar",alpha=0.8,ec="black")
    plt.hist(exponenciales, bins=50, edgecolor="black", histtype="bar",color='darkgrey',alpha=0.8,ec="red")
    plt.show()

exponencial(alfa)

testExpo(exponenciales)


import pymm
import numpy as np
import math
import torch

# based on https://pytorch.org/tutorials/beginner/pytorch_with_examples.html

s = pymm.shelf('myShelf',size_mb=1024,pmem_path='/mnt/pmem0',force_new=True)

dtype = torch.float
device = torch.device("cpu")
# device = torch.device("cuda:0") # Uncomment this to run on GPU

# Create random input and output data
s.x = torch.linspace(-math.pi, math.pi, 2000, dtype=dtype)

# create reference to shelf object; use as_tensor to get the subclass representation
# so that it looks like a Tensor type. this will prevent transactions happening where
# they are not needed
#x = s.x.as_tensor()
s.y = torch.sin(s.x)

#Create random Tensors for weights. For a third order polynomial, we need
# 4 weights: y = a + b x + c x^2 + d x^3
# Setting requires_grad=True indicates that we want to compute gradients with
# respect to these Tensors during the backward pass.
a = torch.randn((), device=device, dtype=dtype, requires_grad=True)
b = torch.randn((), device=device, dtype=dtype, requires_grad=True)
c = torch.randn((), device=device, dtype=dtype, requires_grad=True)
d = torch.randn((), device=device, dtype=dtype, requires_grad=True)

learning_rate = 1e-6
for t in range(2000):
    # Forward pass: compute predicted
    s.y_pred = a + b * s.x + c * s.x ** 2 + d * s.x ** 3

    # Compute and print loss using operations on Tensors.
    # Now loss is a Tensor of shape (1,)
    # loss.item() gets the scalar value held in the loss.
    loss = (s.y_pred - s.y).pow(2).sum()
    if t % 100 == 99:
        print(t, loss.item())

    # Use autograd to compute the backward pass. This call will compute the
    # gradient of loss with respect to all Tensors with requires_grad=True.
    # After this call a.grad, b.grad. c.grad and d.grad will be Tensors holding
    # the gradient of the loss with respect to a, b, c, d respectively.
    loss.backward()

    # Manually update weights using gradient descent. Wrap in torch.no_grad()
    # because weights have requires_grad=True, but we don't need to track this
    # in autograd.
    with torch.no_grad():
        a -= learning_rate * a.grad
        b -= learning_rate * b.grad
        c -= learning_rate * c.grad
        d -= learning_rate * d.grad

    # Manually zero the gradients after updating weights
        a.grad = None
        b.grad = None
        c.grad = None
        d.grad = None

print(f'Result: y = {a.item()} + {b.item()} x + {c.item()} x^2 + {d.item()} x^3')


# File      :CompareHist.py
# Author    :WJ
# Function  :
# Time      :2021/01/11
# Version   :
# Amend     :

import numpy as np
from matplotlib import pyplot as plt

def hist(array=[0,1],bins=20,start=0,end=1):
    hist=np.zeros((bins+1,1))
    x=np.linspace(start=start,stop=end,num=bins+1,endpoint=True)
    for i in range(0,len(array)):
        for j in range(0,len(x)-1):
            if abs(array[i])>=x[j] and abs(array[i])<x[j+1]:
                hist[j]=hist[j]+1
    return hist

def calcSimilarity(hist1, hist2,Similarity=False):
    # 若Simlarity==Ture，则返回相似度，否则返回相异度
    # 计算两直方图的相似度
    # 计算直方图的重合度
    degree = 0
    for i in range(len(hist1)):
        if hist1[i] != hist2[i]:
            degree = degree + \
                (1 - abs(abs(hist1[i]) - abs(hist2[i]))/ max(abs(hist1[i]), abs(hist2[i])))
        else:
            degree = degree + 1
    degree = degree / len(hist1)
    if Similarity==True:
        return degree
    else:
        return 1-degree

def calaHistCorrelation(U1k,U2l,bins=10,start=0,end=1):
    hist1=hist(U1k,bins,start,end)
    hist2=hist(U2l,bins,start,end)
    CorrelationDegree = calcSimilarity(hist1, hist2)
    return CorrelationDegree

if __name__=='__main__':
    U = np.array([-0.1, -0.1, -0.2, -0.9, 0, 0.5, 0.2, 0.6, 0.63, 0.7, 0.8])
    hist1 = hist(U)
    hist2 =np.array(hist1)
    hist2[2]=3
    plt.subplot(1, 2, 1)
    plt.title("hist1")
    plt.plot(hist1)
    plt.subplot(1, 2, 2)
    plt.title("hist2")
    plt.plot(hist2)
    plt.pause(5)
    degree=calcSimilarity(hist1,hist2)
    print(degree)


#!/usr/bin/env python
"""
lhcb_rich_cerenkov_angles_pid.py
===================================

https://physics.stackexchange.com/questions/471722/cherenkov-light-and-refractive-index

"""
import numpy as np, os
import matplotlib.pyplot as plt

def beta(E, mec2):
    gamma = E / mec2
    return np.sqrt(1 - 1 / np.power(gamma, 2))

def theta(beta, n):
    return np.arccos(1 / (beta * n))

mec2_kaon = 493.68  # MeV / c^2
mec2_pion = 139.57  # MeV / c^2
mec2_muon = 105.65  # MeV / c^2

E = np.logspace(np.log10(5e3), 6, 100) # MeV

beta_pion = beta(E, mec2_pion)
beta_kaon = beta(E, mec2_kaon)
beta_muon = beta(E, mec2_muon)

n_1 = 1.0014
n_2 = 1.0005

fig, ax = plt.subplots()

plt.semilogx(E, theta(beta_kaon, n_1), color="crimson", ls="-", label=r"$K, n_1=1.0014$")
plt.semilogx(E, theta(beta_pion, n_1), color="crimson", ls="--", label=r"$\pi, n_1=1.0014$")
plt.semilogx(E, theta(beta_muon, n_1), color="crimson", ls="-.", label=r"$\mu, n_1=1.0014$")

plt.semilogx(E, theta(beta_kaon, n_2), color="k", ls="-", label=r"$K, n_2=1.0005$")
plt.semilogx(E, theta(beta_pion, n_2), color="k", ls="--", label=r"$\pi, n_2=1.0005$")
plt.semilogx(E, theta(beta_muon, n_2), color="k", ls="-.", label=r"$\mu, n_2=1.0005$")

plt.axvline(8e4, color="crimson", ls=":")
plt.axvline(1.1e5, color="k", ls=":")

plt.xlabel("E / MeV")
plt.ylabel(r"$\theta_{\rm C}\,/\,rad$")
plt.legend()
plt.show()

path = "/tmp/qudarap/lhcb_rich_cerenkov_angles_pid.png"
fold = os.path.dirname(path)
if not os.path.isdir(fold):
    os.mkdirs(fold)
pass
fig.savefig(path)


""" Area pattern analysis tests.

Author:

    C.M. Gosmeyer

Date:

    Apr 2018

References:

    "Introduction to Statistical Problem Solving in Geography",
    J.C. McGrew, Jr., A.J. Lembo, Jr., C.B. Monroe
"""

import numpy as np

class JointCountBinary(object):
    """ Determine whether a random process has generated a binary (two-
    category) area pattern.

    Requirements
    ------------
    1. Each area is assigned to one of two categories.
    2. Each pair of areas must be defined as either adjacent ("joined")
    or nonadjacent (not "joined") in a consistent manner.

    Null Hypthothesis
    -----------------
    H0 : ObservedNumberBlack-WhiteJoins = ExpectedNumberBlack-WhiteJoins
        (area pattern is random)
    HA : ObservedNumberBlack-WhiteJoins != ExpectedNumberBlack-WhiteJoins
        (area pattern is NOT random)
    HA : ObservedNumberBlack-WhiteJoins > ExpectedNumberBlack-WhiteJoins
        (area is more dispersed than random)
    HA : ObservedNumberBlack-WhiteJoins < ExpectedNumberBlack-WhiteJoins
        (area pattern is is more clustered than random)

    Test Statistic
    --------------

    Z_b = (ObservedNumberBlack-WhiteJoins - ExpectedNumberBlack-WhiteJoins)
           / StandardErrorExpectedNumberBlack-WhiteJoins

    where

    ExpectedNumberBlack-WhiteJoins = 2*J*B*W / N*(N-1)

    where

    J = total number of joins
    B = number of black areas
    W = number of white areas
    N = total number of areas  = B + W

    and

    StandardErrorExpectedNumberBlack-WhiteJoins = sqrt( ExpectedNumberBlack-WhiteJoins +
        [ sum(L)*(L-1)*B*W) / N(N-1) ] +
        [ 4*{J*(J-1) - sum(L)*(L-1)}*B*(B-1)*W*(W-1) / N*(N-1)*(N-2)*(N-3) ] -
        ExpectedNumberBlack-WhiteJoins^2 )

    where

    L = list of links for each area
    sum(L) = total number of links = 2J
    """
    def __init__(self, B, W, J, link_list, observed_joins):
        """
        Parameters
        ----------
        B : int
            Number of black areas.
        W : int
            Number of white areas.
        J : int
            Total number of joins.
        link_list : list
            List of links for each area.
        observed_joins : int
            Observed number of black-white joins.
        """
        self.B = float(B)
        self.W = float(W)
        self.J = float(J)
        self.observed_joins = float(observed_joins)
        self.link_list = np.asarray(link_list)

        self.N = self.B + self.W
        self.expected_joins = self.return_expected_joins()
        self.sigma_expected_joins = self.return_sigma_expected_joins()
        self.Z_b = None
        self.test_statistic()
        self.test_stat = self.Z_b

    def return_expected_joins(self):
        B = self.B
        W = self.W
        J = self.J
        N = self.N
        expected_joins = 2*J*B*W / (N*(N-1))

        return expected_joins

    def return_sigma_expected_joins(self):
        B = self.B
        W = self.W
        J = self.J
        N = self.N
        L_sums = sum([ L*(L-1) for L in self.link_list ])

        sigma_expected_joins = np.sqrt( self.expected_joins +
            (L_sums*B*W / (N*(N-1))) +
            ((4*(J*(J-1)-L_sums)*B*(B-1)*W*(W-1)) / (N*(N-1)*(N-2)*(N-3))) -
            self.expected_joins**2 )

        return sigma_expected_joins

    def test_statistic(self):
        self.Z_b = (self.observed_joins - self.expected_joins) / self.sigma_expected_joins

    def get_Zb(self):
        print(self.Z_b)

class MoransIndexGlobal(object):
    """ Identify significant spatial patterns within a study area.

    Requirements
    ------------
    1. Minimum of 30 geographic features.
    2. Attribute values measures on an ordinal or interval/ratio scale.

    Null Hypthothesis
    -----------------
    H0 : Attribute values are randomly distributed across features in
    study area.

    Test Statistic
    --------------

    I = n * sum( (x_i - x_mean)*(x_j - x_mean) ) / J * sum( x - x_mean)^2

    where

    n = number of objects
    J = number of joins
    x = area attribute value
    x_mean = mean of all area attribute values
    x_i, x_j = values of contiguous pairs

    Interpretation
    --------------

    Assuming significant p-value:

    I < 0 : observed pattern is dispersed
    I = 0 : observed pattern is random
    I > 0 : observed pattern is clustered

    """
    def __init__(self, area_joins, area_attributes, assumption='normal'):
        """
        Parameters
        ----------
        area_joins : list of ints
            The number of joins for each area.
        area_attributes : list of lists
            The i, j area attributes for each join.
        assumption : str
            Either "normal" or "random"; will determine the calculated
            variance of the distribution.
        """
        self.area_joins = np.asarray(area_joins) # L
        self.area_attributes = np.asarray(area_attributes) # x_i, x_j
        self.assumption = assumption

        self.n = float(len(area_joins))
        self.J = float(len(area_attributes))
        self.I = None
        self.test_statistic()
        self.test_stat = self.I
        self.var_I = self.variance()
        self.z_score = self.z_score()

    def test_statistic(self):
        xs = list(set([x[0] for x in self.area_attributes]))
        x_mean = np.mean(xs)
        x_sum_pairs = sum( [(x_i-x_mean)*(x_j-x_mean) for x_i, x_j in self.area_attributes] )
        x_variance_attribute_vals = sum( [(x-x_mean)**2 for x in xs] )
        self.I = (self.n * x_sum_pairs) / (self.J *  x_variance_attribute_vals)

    def variance(self):
        sum_L2 = float(sum(self.area_joins**2))
        if self.assumption == 'normal':
            var_I = (self.J*self.n**2 + 3*self.J**2 - self.n*sum_L2) / \
                    (self.J**2 * (self.n**2 - 1.))

        elif self.assumption == 'random':
            xs = list(set([x[0] for x in self.area_attributes]))
            x_mean = np.mean(xs)
            # Now calculate kurtosis.
            k = sum( [(x-x_mean)**4 for x in xs] ) / (len(xs) * np.std(xs)**4) - 3
            var_I = ( self.n * ( self.J*(self.n**2+3-3*self.n) + \
                               + 3*self.J**2 - self.n*sum_L2 ) - \
                      k * ( self.J*(self.n**2 - self.n) + \
                            6*self.J**2 - \
                            2*self.n*sum_L2 ) ) / \
                    ( self.J**2*(self.n-1)*(self.n-2)*(self.n-3) )

        else:
            var_I = 0

        return var_I

    def z_score(self):
        expected_I = -1. / (self.n - 1.)
        z_score = (self.I - expected_I) / np.sqrt(self.var_I)
        return z_score


from matplotlib.pyplot import step
import numpy as np

def soft_th(w, th):
    """
    w: input vector
    th: threshold

        soft_th(w, th) = sgn(w) * (|w| - th)^+

    """
    return np.sign(w) * np.maximum(0, np.abs(w) - th)

def ista(X, y, lambd, w0, stepsize, tol=1e-9, maxiter=100000):
    """
    This function implements the iterative soft-thresholding algorithm (ISTA) for
    solving Lasso in penalized form

        minimize 0.5 * ||X * w - y||_2^2 + lambd * ||w||_1

    w0: initial point
    stepsize: constant step size used in ISTA
    tol: toleracne parameter in the stopping criterion. Here we stop when the
         norm of the difference between two succesive iterates is smaller than tol
    maxiter: maximum number of iterations in ISTA.
    """

    w_traces = [np.array(w0)]
    w = np.array(w0)

    for k in range(maxiter):
        w_next = soft_th(w - stepsize * X.T @ (X @ w - y), stepsize * lambd)

        if np.linalg.norm(w_next - w) < tol:
            break

        w = w_next
        w_traces.append(np.array(w))

    return w_traces


import numpy as np
from .tomohelpers import *

"""
purity(rho):

calculates the purity of rho. Defined as the trace of rho ** 2

Parameters:
----------------------
rho: np.matrix

"""

def purity(rho):
    purity = np.trace(np.matmul(rho,rho))
    return np.round(np.real(purity), 3)

"""
s_param(n, rho)

Calculate the s-parameter. Currently only available for two qubit systems.

Parameters:
----------------------

n: int
    number of qubits in the system.

rho: numpy array
    numpy array representing the density matrix of a quantum state

"""

def s_param(n, rho):

    X = np.matrix([[0,1], [1,0]])
    Z = np.matrix([[1,0], [0,-1]])

    M1 = -1/np.sqrt(2)*(Z+X)
    M2 = -1/np.sqrt(2)*(Z-X)

    s = 0

    if n == 2:
        ops = [np.kron(X, M1),np.kron(X, M2),np.kron(Z, M1),np.kron(Z, M2)]
        for i in ops:
            expval = np.trace(np.matmul(rho,i))
            s += abs(expval)

        return np.round(s, 3)

    else:
        print("S-parameter: This is only available for two-qubit systems. S-param was not calculated")

"""
concurrence(rho)

Calculate the concurrence. Currently only available for two qubit systems.

Parameters:
----------------------

rho: numpy array
    numpy array representing the density matrix of a quantum state

"""

def concurrence(rho):
    eigvals, eigvecs = np.linalg.eig(rho)
    eigvals = np.sort(eigvals)[::-1]
    con = eigvals[0]
    for i in eigvals:
        if i == eigvals[0]:
            continue
        else:
            con = con - i

    return max(0, np.round(np.real(con), 3))

"""
tangle(concurrence)

Calculate the tangle. Currently only available for two qubit systems.

Parameters:
----------------------

concurrence: num
    The value of the concurrence of the system.

"""

def tangle(concurrence):
    return concurrence ** 2

"""
stokes_params(rho, d)

Calculates the Stoke's Parameters of a given quantum state density matrix.

Parameters:
----------------------

rho: numpy array
    numpy array representing the density matrix of a quantum state

d: int
    represents the dimension of our quantum system

n: int
    number of qudits in the system

"""

def stokes_params(rho, d, n):
    stokes_params = []
    gell_manns = generate_gellman(d)
    sp_matricies = gen_sp_matricies(n, gell_manns, gell_manns)

    for i in sp_matricies:
        #print(i, np.size(i), rho, np.size(rho))
        stoke_param = np.trace(np.matmul(i, rho))
        stokes_params.append(round(stoke_param, 3))

    return stokes_params


from sklearn import preprocessing
import numpy as np
import math

X_train = np.array([[ 1., -1.,  2.],
                    [ 2.,  0.,  0.],
                    [ 0.,  1., -1.]])
# X_scaled = preprocessing.scale(X_train)
mm = preprocessing.StandardScaler()
mm_data = mm.fit_transform(X_train)
mean = mm.mean_[-1]
std = math.sqrt(mm.var_[-1])
trans = mm_data[:, 2]
func = np.vectorize(lambda x: x * std + mean)
func(trans)
# predict = [9,2,10]
# origin_data = mm.inverse_transform(mm_data[:, 2])


"""Test diffusion example."""
import numpy as np
from pyamg.gallery.diffusion import diffusion_stencil_2d

from numpy.testing import TestCase, assert_equal, assert_almost_equal

class TestDiffusionStencil2D(TestCase):
    def test_simple_finite_difference(self):
        # isotropic
        stencil = [[0.0, -1.0, 0.0],
                   [-1.0, 4.0, -1.0],
                   [0.0, -1.0, 0.0]]
        assert_equal(diffusion_stencil_2d(epsilon=1.0, theta=0.0,
                                          type='FD'), stencil)

        # weak horizontal
        stencil = [[0.0, -1.0, 0.0],
                   [-0.5, 3.0, -0.5],
                   [0.0, -1.0, 0.0]]
        assert_almost_equal(diffusion_stencil_2d(epsilon=0.5, theta=0.0,
                                                 type='FD'), stencil)

        # weak vertical
        stencil = [[0.0, -0.5, 0.0],
                   [-1.0, 3.0, -1.0],
                   [0.0, -0.5, 0.0]]
        assert_almost_equal(diffusion_stencil_2d(epsilon=0.5, theta=np.pi/2,
                                                 type='FD'), stencil)

    def test_simple_finite_element(self):
        # isotropic
        stencil = np.array([[-1.0, -1.0, -1.0],
                            [-1.0, 8.0, -1.0],
                            [-1.0, -1.0, -1.0]]) / 3.0
        assert_almost_equal(diffusion_stencil_2d(epsilon=1.0, theta=0.0,
                                                 type='FE'), stencil)

        # weak horizontal
        # assert_almost_equal(diffusion_stencil_2d(epsilon=0.5, theta=0.0,
        # type='FE'), stencil)

        # weak vertical
        # assert_almost_equal(diffusion_stencil_2d(epsilon=0.5, theta=pi/2,
        # type='FE'), stencil)

    def test_zero_sum(self):
        """Test that stencil entries sum to zero."""
        for type in ['FD', 'FE']:
            for theta in [np.pi/8, np.pi/5, np.pi/4, np.pi/3, np.pi/2, np.pi]:
                for epsilon in [0.001, 0.01, 1.0]:
                    stencil = diffusion_stencil_2d(epsilon=epsilon,
                                                   theta=theta, type=type)
                    assert_almost_equal(stencil.sum(), 0.0)

    def test_rotation_invariance(self):
        """Test invariance to theta when epsilon=1.0."""
        for type in ['FD', 'FE']:
            expected = diffusion_stencil_2d(epsilon=1.0, theta=0.0, type=type)
            for theta in [np.pi/8, np.pi/4, np.pi/3, np.pi/2, np.pi]:
                result = diffusion_stencil_2d(epsilon=1.0,
                                              theta=theta, type=type)
                assert_almost_equal(result, expected)


#!/usr/bin/env python3

import numpy as np
import sklearn.preprocessing as preprocessing

def mapFeature(X1, X2):
    # MAPFEATURE Feature mapping function to polynomial features
    #
    #   MAPFEATURE(X1, X2) maps the two input features
    #   to quadratic features used in the regularization exercise.
    #
    #   Returns a new feature array with more features, comprising of
    #   X1, X2, X1.^2, X2.^2, X1*X2, X1*X2.^2, etc..
    #
    #   Inputs X1, X2 must be the same size
    #

    # n = X1.shape[0]
    # degree = 6
    # out = np.ones((n, 1)).reshape((n, 1))
    # for i in range(1, degree + 1):
    #     for j in range(i + 1):
    #         term1 = X1 ** (i - j)
    #         term2 = X2 ** j
    #         out = np.hstack((out, (term1 * term2).reshape((n, 1))))

    data = np.c_[X1, X2]
    poly = preprocessing.PolynomialFeatures(6)
    out = poly.fit_transform(data)

    return out


# -*- coding: utf-8 -*-
"""GMM_EM.ipynb

Automatically generated by Colaboratory.

# Gaussian mixture model with expectation maximization algorithm

GMM with EM.

This notebook implements the following:

1) Function that avoids computing inverse of matrix when computing y=(A_inverse)x by solving system of linear equations.

2) Log sum trick to avoid underflow when multiplying small numbers.

3) pdf of the Multivariate normal distribution

4) E-step function of the EM algorithm

5) M-step function of the EM algorithm

6) Variational lower bound function

7) GMM function

8) Training function for GMM

9) Scatter plot of clusters (Plot at the bottom of this notebook shows 8 clusters from a dataset of 100 points)

# Imports
"""

# Commented out IPython magic to ensure Python compatibility.
import sys
import numpy as np
from numpy.linalg import det, solve
import matplotlib
import matplotlib.pyplot as plt
# %matplotlib inline

"""# Variables info

N: number of data (rows)

d: dimension of data

X: (N x d), data points

C: int, number of clusters

Computed from E-step:

gamma: (N x C), distribution q(T), probabilities of clusters for objects

Initial values are also subsequently computed & updated from M-step:

pi: (C), mixture component weights, initial weights of T (latent variable), sum to 1, t=1, 2 or 3.
mu: (C x d), mixture component means

sigma: (C x d x d), # mixture component covariance matrices

# Generate random data.
"""

N = 100
d = 2

X = np.random.rand(N,d)

print(X[:5])
print(X.shape)

fig, ax = plt.subplots(1,1, figsize=(15,10))
ax.set_title('Data')
ax.scatter(X[:, 0], X[:, 1], c='black', s=100)
#plt.axis('equal')
plt.show()

"""# Generate initial values"""

epsilon = 1e-10 # Use in stopping criterion as well as in preventing numerical errors.

C = 7

def rand_input(C, d):
  # https://stackoverflow.com/questions/18659858/generating-a-list-of-random-numbers-summing-to-1
  pi0 = np.random.dirichlet(np.ones(C),size=1)[0] # Generating a list of random numbers, summing to 1
  mu0 = np.random.rand(C,d)
  sigma0 = np.random.rand(C,d,d)
  return pi0, mu0, sigma0

pi0, mu0, sigma0 = rand_input(C, d)

print(pi0)
print(pi0.shape)
print(mu0)
print(mu0.shape)
print(sigma0)
print(sigma0.shape)

"""# Avoid computing inverse of matrix when computing y=(A_inverse)x."""

# Function which avoids computing inverse of matrix when computing y=(A_inverse)x by solving linear equations.
# Use in E-step.

def _A_inverse_times_X(A, X):
  # A is nxn
  # X is rxn

  Y = []
  for row_data in X:
    Y_new = np.linalg.solve(A, row_data)
    Y.append(Y_new)

  Y = np.asarray(Y)
  assert Y.shape == X.shape, "Output shape must be equal to shape of X."

  return Y

"""# Multivariate normal (Gaussian) distribution

$MVN = \frac{1}{\sqrt{(2\pi)^n|\boldsymbol\Sigma_c|}}
\exp\left(-\frac{1}{2}({x}-{\mu_c})^T{\boldsymbol\Sigma_c}^{-1}({x}-{\mu_c})\right)$

Computes pdf of Multivariate normal (Gaussian) distribution.
"""

# Alternatively, one could also use multivariate_normal.pdf from scipy.stats
# instead of this function

def __mvg(cov, X, mean):
  diff = X - mean
  Y = _A_inverse_times_X(cov, diff)
  pow_term = -0.5 * np.matmul(Y, diff.T)
  e_term = np.exp(pow_term)

  const_term = (2*np.pi)**(X.shape[1])
  det_term = np.linalg.det(cov)
  deno_term = np.sqrt(np.multiply(const_term, det_term))

  P = np.divide(e_term, deno_term)

  return P.diagonal() # returns the pdf, shape=(num_X,)

# Returns pdf for multiple components.

def _mvg(cov, X, mean):
  P = []
  for i, r in enumerate(mean):
    P.append(__mvg(cov[i], X, mean[i]))

  return P # shape=(C, num_X)

"""# Log sum trick"""

# log sum trick to prevent underflow in E-step.
# https://timvieira.github.io/blog/post/2014/02/11/exp-normalize-trick/
# https://web.archive.org/web/20150502150148/http://machineintelligence.tumblr.com/post/4998477107/the-log-sum-exp-trick
# https://www.quora.com/Why-is-e-log_-e-x-equal-to-x

def exp_normalize(x):
  b = x.max()
  y = np.exp(x - b)

  return y / y.sum()

"""# E-step

Multiply the initial weight of the class with the multivariate guassian pdf of each data from the class.
"""

def E_step(X, pi, mu, sigma):
    N = X.shape[0] # number of objects
    C = pi.shape[0] # number of clusters
    d = mu.shape[1] # dimension of each object
    gamma = np.zeros((N, C)) # distribution q(T)
    gamma = np.mat(np.zeros((N, C)))

    prob = _mvg(sigma, X, mu) # pdf of data X in each class
    prob = np.mat(prob)

    for c in range(C):
        # Instead of multiplying probabilities directly which could result in underflow,
        # we'll work in log scale.
        # pi[c] = P(T=c), prob[c, :] = P(X|T=c)
        #gamma[:, c] = np.multiply(pi[c], prob[c, :].T)
        gamma[:, c] = np.log(pi[c] + epsilon) + np.log(prob[c, :].T + epsilon)

    for i in range(N):
        # Instead of summing the denominator, we'll use the log sum trick coded in exp_normalize function.
        gamma[i, :] = exp_normalize(gamma[i, :])

    return gamma # Q(T) = P(T|X,theta), weights of each model (class) for each data in X.

"""# M-Step

Compute the following:

[Equations from wiki](https://en.wikipedia.org/wiki/Expectation%E2%80%93maximization_algorithm#E_step)

![alt text](https://wikimedia.org/api/rest_v1/media/math/render/svg/0e0327c8676ae66ec651b422a19f5ea532913c7a)

![alt text](https://wikimedia.org/api/rest_v1/media/math/render/svg/45f3e73f50d396aadc98182709eee0c0d513aa6b)

![alt text](https://wikimedia.org/api/rest_v1/media/math/render/svg/a92651be432155520db19dc0b4da807039d96eb0)
"""

def M_step(X, gamma):
    N = X.shape[0] # number of objects
    C = gamma.shape[1] # number of clusters
    d = X.shape[1] # dimension of each object

    mu = np.zeros((C, d))
    sigma = []
    pi = np.zeros(C)

    # for each model in C
    for c in range(C):
        # sum of all Q(t) of model c
        sum_Q_t = np.sum(gamma[:, c])

        # mean of model c
        mu[c, :] = np.sum(np.multiply(X, gamma[:, c]), axis=0) / sum_Q_t

        # cov of model c
        diff = X - mu[c]
        sigma.append(diff.T @ np.multiply(diff, gamma[:, c]) / sum_Q_t)

        # weight of model c
        pi[c] = sum_Q_t / N

    return pi, mu, np.asarray(sigma)

"""# Variational lower bound

Computes the scalar output of the following:

$$\sum_{i=1}^{N} \sum_{c=1}^{C} q(t_i =c) (\log \pi_c + \log(MVN)) - \sum_{i=1}^{N} \sum_{c=1}^{K} q(t_i =c) \log q(t_i =c)$$
"""

def compute_vlb(X, pi, mu, sigma, gamma):
    """
    Each input is numpy array:
    X: (N x d), data points
    gamma: (N x C), distribution q(T)
    pi: (C)
    mu: (C x d)
    sigma: (C x d x d)

    Returns value of variational lower bound
    """
    N = X.shape[0] # number of objects
    C = gamma.shape[1] # number of clusters
    d = X.shape[1] # dimension of each object

    VLB = 0.0
    for c in range(C):
      mu_c = np.expand_dims(mu[c,:], axis=0)
      sigma_c = np.expand_dims(sigma[c,:], axis=0)
      gamma_c = gamma[:,c]

      mvg = np.asarray(_mvg(sigma_c, X, mu_c)) # 1xc
      sum = np.log(pi[c] + epsilon) + np.log(mvg + epsilon) # 1xc, + 1e-30 to prevent log(0)
      prod = np.multiply(gamma_c, sum.T) # transpose sum for element wise multiplication
      prod2 = np.multiply(gamma_c, np.log(gamma_c + epsilon)) # element wise multiplication, + 1e-30 to prevent log(0)
      VLB += (prod - prod2)

    VLB = np.sum(VLB, axis=0) # sum all values for all rows

    return VLB

"""# GMM

Find the best parameters by optimizing with the following criterion:

Stopping threshold: ($|\frac{\mathcal{L}_i-\mathcal{L}_{i-1}}{\mathcal{L}_{i-1}}| \le \text{threshold}$)
"""

def GMM(X, C, d, threshold=epsilon, max_iter=1000, trial=500):
    N = X.shape[0] # number of objects
    d = X.shape[1] # dimension of each object
    best_VLB = None
    best_pi = None
    best_mu = None
    best_sigma = None

    for rs in range(trial):
        try:
            pi, mu, sigma = rand_input(C, d) # Try random initial values
            curr_LVB, prev_LVB = 0.0, 0.0
            iter = 0
            while iter < max_iter:
                #print('iter, rs', iter, rs)

                prev_LVB = curr_LVB

                gamma = E_step(X, pi, mu, sigma)
                pi, mu, sigma = M_step(X, gamma)
                curr_LVB = compute_vlb(X, pi, mu, sigma, gamma)

                #print('prev_LVB', prev_LVB)
                #print('curr_LVB', curr_LVB)

                # LVB is the variation lower bound function. It must NOT be decreasing.
                # We are trying to maximize LVB so that the gap between LVB & GMM is minimized.
                if prev_LVB != 0.0 and curr_LVB < prev_LVB:
                    print('VLB ERROR EXIT!: curr_LVB < prev_LVB')
                    sys.exit(1)

                # If numerical error in LVB, goto next trial.
                if np.isnan(curr_LVB) == True:
                    break

                if prev_LVB != 0.0 and abs((curr_LVB - prev_LVB) / (prev_LVB)) <= threshold:
                    if best_VLB == None or curr_LVB > np.float32(best_VLB):
                        best_VLB = curr_LVB
                        best_pi = pi
                        best_mu = mu
                        best_sigma = sigma
                    break # end while loop, goto for loop

                iter += 1

        except np.linalg.LinAlgError:
            print("Singular matrix not allowed.")
            pass

    return best_VLB, best_pi, best_mu, best_sigma

"""# Train"""

# Train
# If numerical errors occured, run a couple of more times.

best_VLB, best_pi, best_mu, best_sigma = GMM(X, C, d)
print('best_VLB', best_VLB)
print('best_pi', best_pi)
print('best_mu', best_mu)
print('best_sigma', best_sigma)

# Use the best values to do 1 more E-step to get gamma.
gamma = E_step(X, best_pi, best_mu, best_sigma)
labels = np.ravel(gamma.argmax(axis=1))

"""# Scatter plot"""

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

'''
# Generate colors for each class.
# only works for max of 4 classes.
def gen_col(C):
  colors =[]
  for c in range(C):
    colors.append(np.random.randint(0, 255, C) / 255)
  print(colors)
  return colors

colors = gen_col(C)

plt.scatter(X[:, 0], X[:, 1], c=labels, cmap=matplotlib.colors.ListedColormap(colors), s=30)
plt.axis('equal')
plt.show()
'''

# https://stackoverflow.com/questions/12487060/matplotlib-color-according-to-class-labels

N = C # Number of labels

# setup the plot
fig, ax = plt.subplots(1,1, figsize=(15,10))
# define the data
x = X[:, 0]
y = X[:, 1]
tag = labels

# define the colormap
cmap = plt.cm.jet
# extract all colors from the .jet map
cmaplist = [cmap(i) for i in range(cmap.N)]
# create the new map
cmap = cmap.from_list('Custom cmap', cmaplist, cmap.N)

# define the bins and normalize
bounds = np.linspace(0,N,N+1)
norm = mpl.colors.BoundaryNorm(bounds, cmap.N)

# make the scatter
scat = ax.scatter(x, y, c=tag, s=np.random.randint(100,500,N), cmap=cmap, norm=norm)

# create the colorbar
cb = plt.colorbar(scat, spacing='proportional',ticks=bounds)
cb.set_label('Custom cbar')
ax.set_title('Discrete color mappings')
plt.show()


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MIT License

Copyright (c) 2022 Jongrae.K

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

import numpy as np
from numpy import linspace
from scipy.integrate import solve_ivp

init_time = 0 # [s]
final_time = 60.0 # [s]
num_data = 1000
tout = linspace(init_time, final_time, num_data)

q0 = np.array([0,0,0,1])
RelTol_cases = np.array([1e-3, 1e-6, 1e-9])
N_sim = len(RelTol_cases)

def dqdt_attitude_kinematics(time, state):
     quat = state
     w_true = np.array([0.1*np.sin(2*np.pi*0.005*time), #[rad/s]
                        0.05*np.cos(2*np.pi*0.01*time + 0.2), #[rad/s]
                        0.02]) #[rad/s]

     wx=np.array([[0,           -w_true[2],     w_true[1]],
                  [w_true[2],   0,              -w_true[0]],
                  [-w_true[1],  w_true[0],      0]])

     Omega_13 = np.hstack((-wx,np.resize(w_true,(3,1))))
     Omega_4  = np.hstack((-w_true,0))
     Omega = np.vstack((Omega_13, Omega_4))

     dqdt = 0.5*(Omega@quat)

     return dqdt

err_idx = np.zeros((num_data,N_sim))
for idx in range(N_sim):
    sol = solve_ivp(dqdt_attitude_kinematics, (init_time, final_time), q0, t_eval=tout, rtol=RelTol_cases[idx], atol=RelTol_cases[idx]*1e-3)
    qout = sol.y
    err_idx[:,idx]=np.abs(np.sum(qout**2,0)-1)

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot(tout,np.log(err_idx[:,0]+1e-100),'b-',
        tout,np.log(err_idx[:,1]+1e-100),'r--',
        tout,np.log(err_idx[:,2]+1e-100),'g-.')

fig.set_figheight(6) # size in inches
fig.set_figwidth(8)  # size in inches

xtick_list = np.array([0,10,20,30,40,50,60])
ax.set_xticks(xtick_list)
ax.set_xticklabels(xtick_list,fontsize=14)

ytick_list = np.array([-35,-30,-25,-20,-15,-10,-5,0])
ax.set_yticks(ytick_list)
ax.set_yticklabels(ytick_list,fontsize=14)

ax.axis((0,60,-35,0))
ax.set_xlabel('time [s]',fontsize=14)
ax.set_ylabel(r'$\log|{\bf q}^T{\bf q}-1|$',fontsize=14)

ax.legend(('RelTol=1e-3','RelTol=1e-6','RelTol=1e-9'),fontsize=14, loc='lower right')


# -*- coding: utf-8 -*-
"""
Created on Sat Jul 13 17:54:48 2019

@author: Marcus Bonifacio

Esse script é a tarefa 8 da aula de Bases Computacionais
"""

print('1) Mostrar a classificação do IMC para as seguintes pessoas:') #enunciado
print('Pessoa a: m = 52Kg e h = 1,58m')
print('Pessoa b: m = 83Kg e h = 1,75m')
print('Pessoa c: m = 70Kg e h = 1,79m')
print('\n')

import numpy as np #importa a biblioteca numpy para o script
import matplotlib.pyplot as plt

#Pessoa a
IMCa1 = 52/(1.58**2) #cálculo para o imc da pessoa a
IMCa = np.round(IMCa1, 1) #arrendondamento do valor do imc para uma casa decimal
if IMCa < 17: #linhas que comparam o valor do imc e definem a categoria que a pessoa se enquadra
    print('Pessoa a : IMC =',IMCa,'; Muito abaixo do peso')
else:
    if IMCa >= 17 and IMCa < 18.5:
        print('Pessoa a : IMC =',IMCa,'; Abaixo do peso')
    else:
        if IMCa >= 18.5 and IMCa < 25:
            print('Pessoa a : IMC =',IMCa,'; Peso normal')
        else:
            if IMCa >= 25 and IMCa < 30:
                print('Pessoa a : IMC =',IMCa,'; Acima do peso')
            else:
                if IMCa >= 30 and IMCa < 35:
                    print('Pessoa a : IMC =',IMCa,'; Obesidade Grau I')
                else:
                    if IMCa >= 35 and IMCa < 40:
                        print('Pessoa a : IMC =',IMCa,'; Obesidade Grau II')
                    else:
                        print('Pessoa a : IMC =',IMCa,'; Obesidade Grau III')

#Pessoa b
IMCb1 = 83/(1.75**2) #cálculo para o imc da pessoa b
IMCb = np.round(IMCb1, 1) #arrendondamento do valor do imc para uma casa decimal
if IMCb < 17: #linhas que comparam o valor do imc e definem a categoria que a pessoa se enquadra
    print('Pessoa b : IMC =',IMCb,'; Muito abaixo do peso')
else:
    if IMCb >= 17 and IMCb < 18.5:
        print('Pessoa b : IMC =',IMCb,'; Abaixo do peso')
    else:
        if IMCb >= 18.5 and IMCb < 25:
            print('Pessoa b : IMC =',IMCb,'; Peso normal')
        else:
            if IMCb >= 25 and IMCb < 30:
                print('Pessoa b : IMC =',IMCb,'; Acima do peso')
            else:
                if IMCb >= 30 and IMCb < 35:
                    print('Pessoa b : IMC =',IMCb,'; Obesidade Grau I')
                else:
                    if IMCb >= 35 and IMCb < 40:
                        print('Pessoa b : IMC =',IMCb,'; Obesidade Grau II')
                    else:
                        print('Pessoa b : IMC =',IMCb,'; Obesidade Grau III')

#Pessoa c
IMCc1 = 70/(1.79**2) #cálculo para o imc da pessoa c
IMCc = np.round(IMCc1, 1) #arrendondamento do valor do imc para uma casa decimal
if IMCc < 17: #linhas que comparam o valor do imc e definem a categoria que a pessoa se enquadra
    print('Pessoa c : IMC =',IMCc,'; Muito abaixo do peso')
else:
    if IMCc >= 17 and IMCc < 18.5:
        print('Pessoa c : IMC =',IMCb,'; Abaixo do peso')
    else:
        if IMCc >= 18.5 and IMCc < 25:
            print('Pessoa c : IMC =',IMCc,'; Peso normal')
        else:
            if IMCc >= 25 and IMCc < 30:
                print('Pessoa c : IMC =',IMCc,'; Acima do peso')
            else:
                if IMCc >= 30 and IMCc < 35:
                    print('Pessoa c : IMC =',IMCc,'; Obesidade Grau I')
                else:
                    if IMCc >= 35 and IMCc < 40:
                        print('Pessoa c : IMC =',IMCc,'; Obesidade Grau II')
                    else:
                        print('Pessoa c : IMC =',IMCc,'; Obesidade Grau III')

print('\n')
print('\n')

print('2) A acidez A(x) de uma solução de hidróxido de magnésio em ácido clorídrico, sob certas condições experimentais, é dada pela equação:'

'A(x)=x³+3x²−54'
' na qual x é a concentração de íons hidrônio.'

' Pede-se:'

' 1) Gere o gráfico de A(x) em função de x para 0 ⩽x⩽8.'

' 2) Determine a concentração x do íon de hidrônio que resulta em solução saturada (i.e., com acidez nula). Acrescente uma instrução que gere um ponto vermelho no gráfico correspondente à saturação nula da solução.')
print('\n')

x1 = np.arange(0, 8.1, 0.1) #cria um vetor com os valores da concentração de hidrônio

Ax = (x1**3) + (3*(x1**2)) - 54 #definicão da acidez da solução em função da concentração

plt.figure()
plt.plot(x1, Ax, color = 'red', marker = 's', markevery = np.arange(len(x1))[Ax==0]) #configurações visuais do gráfico
plt.title('Acidez da solução de Mg(OH)\u2082 em HCl') #título do gráfico
plt.xlabel('Concentração de H\u2083O') #rótulo do eixo x
plt.ylabel('Acidez') #rótulo do eixo y
plt.show()

print('Concentração de H\u2083O quando a acidez é 0 =',x1[Ax==0])

print('\n')
print('\n')

print('3) Faça o gráfico de:'
' f(x)=|x−2|+|2x+1|−x−6'
' Faça o gráfico com x variando de -10 a 10.'

' Mostre no Console o menor valor de x tal que f(x)>0 e x>0.')

x2 = np.arange(-10, 10.1, 0.1) #cria vetor com os valores de x

y2 = (np.abs(x2-2)) + (np.abs((2*x2)+1)) - x2 - 6 #função para o vetor com valores de f(x)

plt.figure() #gráfico da função
plt.plot(x2, y2, color='green')
plt.xlabel('x')
plt.ylabel('f(x)')
plt.title('Gráfico de f(x)=|x−2|+|2x+1|−x−6')
plt.show()

z2 = np.logical_and(x2 > 0, y2 >= 0) #condição para criação do vetor onde f(x) e x > 0

xm2 = x2[z2] #cria o vetor com os elementos em que f(x) e x > 0

xmin = np.min(xm2) #valor mínimo do vetor acima
print('Valor mínimo de x para f(x) e x > 0 =',np.round(xmin,1))

print('\n')
print('\n')

print('3) Faça o gráfico da seguinte função:'

' f(x)=x²–sin(0.784x²)–2'
' Mostre no Console as raízes da função (i.e. os valores de x para os quais f(x)=0).')

x3 = np.round(np.arange(-5, 5.1, 0.001),3) #cria um vetor com valores de x

y3 = (x3**2)-(np.sin(0.784*(x3**2)))-2 #define os valores de f(x)

plt.figure() #gráfico da função
plt.plot(x3, y3, color='blue')
plt.xlabel('x')
plt.ylabel('f(x)')
plt.title('Gráfico de f(x)=x²–sin(0.784x²)–2')
plt.show()

print('Raízes da função: x =',x3[np.logical_and(y3 > -0.002, y3 < 0.002)]) #busca os valores de x quando f(x) = 0
#como não há valor exato 0 no vetor, busca-se o valor mais próximo

"""
Fim do Script
"""


"""
Discrete probability distributions.
"""
from math import ceil

import numpy as np
from scipy import stats

from .distributions import Discrete

eps = np.finfo(float).eps

class Binomial(Discrete):
    R"""
    Binomial distribution.

    The discrete probability distribution of the number of successes
    in a sequence of n independent yes/no experiments, each of which
    yields success with probability p.

    The pmf of this distribution is

    .. math:: f(x \mid n, p) = \binom{n}{x} p^x (1-p)^{n-x}

    .. plot::
        :context: close-figs

        import arviz as az
        from preliz import Binomial
        az.style.use('arviz-white')
        ns = [5, 10, 10]
        ps = [0.5, 0.5, 0.7]
        for n, p in zip(ns, ps):
            Binomial(n, p).plot_pdf()

    ========  ==========================================
    Support   :math:`x \in \{0, 1, \ldots, n\}`
    Mean      :math:`n p`
    Variance  :math:`n p (1 - p)`
    ========  ==========================================

    Parameters
    ----------
    n : int
        Number of Bernoulli trials (n >= 0).
    p : float
        Probability of success in each trial (0 < p < 1).
    """

    def __init__(self, n=None, p=None):
        super().__init__()
        self.n = n
        self.p = p
        self.name = "binomial"
        self.params = (self.n, self.p)
        self.param_names = ("n", "p")
        self.params_support = ((eps, np.inf), (eps, 1 - eps))
        self.dist = stats.binom
        self._update_rv_frozen()

    def _get_frozen(self):
        return self.dist(self.n, self.p)

    def _update(self, n, p):
        self.n = n
        self.p = p
        self.params = (n, p)
        self._update_rv_frozen()

    def fit_moments(self, mean, sigma):
        p = 1 - sigma**2 / mean
        n = int(mean / p)
        self._update(n, p)

    def fit_mle(self, sample):
        # see https://doi.org/10.1016/j.jspi.2004.02.019 for details
        x_bar = np.mean(sample)
        x_std = np.std(sample)
        x_max = np.max(sample)
        n = ceil(x_max ** (1.5) * x_std / (x_bar**0.5 * (x_max - x_bar) ** 0.5))
        p = x_bar / n
        self._update(n, p)

class NegativeBinomial(Discrete):
    R"""
    Negative binomial distribution.

    If it is parametrized in terms of n and p, the negative binomial describes
    the probability to have x failures before the n-th success, given the
    probability p of success in each trial. Its pmf is

    .. math::

        f(x \mid n, p) =
           \binom{x + n - 1}{x}
           (p)^n (1 - p)^x

    .. plot::
        :context: close-figs

        import arviz as az
        from preliz import NegativeBinomial
        az.style.use('arviz-white')
        ns = [5, 10, 10]
        ps = [0.5, 0.5, 0.7]
        for n, p in zip(ns, ps):
            NegativeBinomial(n, p).plot_pdf()

    ========  ==========================
    Support   :math:`x \in \mathbb{N}_0`
    Mean      :math:`\frac{(1-p) r}{p}`
    Variance  :math:`\frac{(1-p) r}{p^2}`
    ========  ==========================

    Parameters
    ----------
    n: float
        Number of target success trials (n > 0)
    p: float
        Probability of success in each trial (0 < p < 1).
    """

    def __init__(self, n=None, p=None):
        super().__init__()
        self.n = n
        self.p = p
        self.name = "negativebinomial"
        self.params = (self.n, self.p)
        self.param_names = ("n", "p")
        self.params_support = ((eps, np.inf), (eps, 1 - eps))
        self.dist = stats.nbinom
        self._update_rv_frozen()

    def _get_frozen(self):
        return self.dist(self.n, self.p)

    def _update(self, n, p):
        self.n = n
        self.p = p
        self.params = (self.n, p)
        self._update_rv_frozen()

    def fit_moments(self, mean, sigma):
        n = mean**2 / (sigma**2 - mean)
        p = mean / sigma**2
        self._update(n, p)

    def fit_mle(self, sample):
        raise NotImplementedError

class Poisson(Discrete):
    R"""
    Poisson log-likelihood.

    Often used to model the number of events occurring in a fixed period
    of time when the times at which events occur are independent.
    The pmf of this distribution is

    .. math:: f(x \mid \mu) = \frac{e^{-\mu}\mu^x}{x!}

    .. plot::
        :context: close-figs

        import arviz as az
        from preliz import Poisson
        az.style.use('arviz-white')
        for mu in [0.5, 3, 8]:
            Poisson(mu).plot_pdf()

    ========  ==========================
    Support   :math:`x \in \mathbb{N}_0`
    Mean      :math:`\mu`
    Variance  :math:`\mu`
    ========  ==========================

    Parameters
    ----------
    mu: float
        Expected number of occurrences during the given interval
        (mu >= 0).

    Notes
    -----
    The Poisson distribution can be derived as a limiting case of the
    binomial distribution.
    """

    def __init__(self, mu=None):
        super().__init__()
        self.mu = mu
        self.name = "poisson"
        self.params = (self.mu,)
        self.param_names = ("mu",)
        self.params_support = ((eps, np.inf),)
        self.dist = stats.poisson
        self._update_rv_frozen()

    def _get_frozen(self):
        return self.dist(self.mu)

    def _update(self, mu):
        self.mu = mu
        self.params = (self.mu,)
        self._update_rv_frozen()

    def fit_moments(self, mean, sigma=None):  # pylint: disable=unused-argument
        self._update(mean)

    def fit_mle(self, sample):
        mu = np.mean(sample)
        self._update(mu)


"""W-state."""
from typing import List

import numpy as np

from scipy import sparse

def w_state(num_qubits: int, coeff: List[int] = None) -> np.ndarray:
    r"""
    Produce a W-state [DVC00]_.

    Returns the W-state described in [DVC00]. The W-state on `num_qubits` qubits is defined by:

    .. math::
        |W \rangle = \frac{1}{\sqrt{num\_qubits}}
        \left(|100 \ldots 0 \rangle + |010 \ldots 0 \rangle + \ldots +
        |000 \ldots 1 \rangle \right).

    Examples
    ==========

    Using :code:`toqito`, we can generate the :math:`3`-qubit W-state

    .. math::
        |W_3 \rangle = \frac{1}{\sqrt{3}} \left( |100\rangle + |010 \rangle +
        |001 \rangle \right)

    as follows.

    >>> from toqito.states import w_state
    >>> w_state(3)
    [[0.    ],
     [0.5774],
     [0.5774],
     [0.    ],
     [0.5774],
     [0.    ],
     [0.    ],
     [0.    ]]

    We may also generate a generalized :math:`W`-state. For instance, here is a
    :math:`4`-dimensional :math:`W`-state

    .. math::
        \frac{1}{\sqrt{30}} \left( |1000 \rangle + 2|0100 \rangle + 3|0010
        \rangle + 4 |0001 \rangle \right).

    We can generate this state in :code:`toqito` as

    >>> from toqito.states import w_state
    >>> import numpy as np
    >>> coeffs = np.array([1, 2, 3, 4]) / np.sqrt(30)
    >>> w_state(4, coeffs)
    [[0.    ],
     [0.7303],
     [0.5477],
     [0.    ],
     [0.3651],
     [0.    ],
     [0.    ],
     [0.    ],
     [0.1826],
     [0.    ],
     [0.    ],
     [0.    ],
     [0.    ],
     [0.    ],
     [0.    ],
     [0.    ]]

    References
    ==========
    .. [DVC00] Three qubits can be entangled in two inequivalent ways.
        W. Dur, G. Vidal, and J. I. Cirac.
        E-print: arXiv:quant-ph/0005115, 2000.

    :param num_qubits: An integer representing the number of qubits.
    :param coeff: default is `[1, 1, ..., 1]/sqrt(num_qubits)`: a
                  1-by-`num_qubts` vector of coefficients.
    """
    if coeff is None:
        coeff = np.ones(num_qubits) / np.sqrt(num_qubits)

    if num_qubits < 2:
        raise ValueError("InvalidNumQubits: `num_qubits` must be at least 2.")
    if len(coeff) != num_qubits:
        raise ValueError(
            "InvalidCoeff: The variable `coeff` must be a vector "
            "of length equal to `num_qubits`."
        )

    ret_w_state = sparse.csr_matrix((2 ** num_qubits, 1)).toarray()

    for i in range(num_qubits):
        ret_w_state[2 ** i] = coeff[num_qubits - i - 1]

    return np.around(ret_w_state, 4)


import numpy as np
from scipy import linalg as LA

class LDA(object):
	def __init__(self, data_inputs, data_labels):
		self.data_inputs = np.array(data_inputs)
		self.data_labels = data_labels
		self.test_cases = self.data_inputs.shape[0]
		self.labels = np.unique(data_labels)
		self.Sw = np.zeros((self.data_inputs.shape[1], self.data_inputs.shape[1]))
		self.Sb = self.Sw.copy()

	def analyse(self):
		C = np.cov(self.data_inputs.T)

		for label in self.labels:
			indices = np.where(self.data_labels == label)
			points = self.data_inputs[indices[0]]
			classcov = np.cov(points.T)
			self.Sw += (np.float(points.shape[0])/self.test_cases) * classcov

		self.Sb = C - self.Sw
		evals, evecs = LA.eig(self.Sw, self.Sb)
		indices = np.argsort(evals)
		indices = indices[::-1]
		evals = evals[indices]
		evecs = evecs[indices]
		self.eigen_vals = evals
		self.eigen_vecs = evecs

	def reduce_dim(self, red_n, data_inputs=None):
		w = self.eigen_vecs[:,:red_n]
		if data_inputs is None:
			data_inputs = self.data_inputs
		return np.dot(data_inputs, w)

	def expand_dim(self, red_data):
		red_n = red_data.shape[1]
		return np.transpose(np.dot(self.eigen_vecs[:,:red_n], red_data.T))


from sympy import *
from sympy_recursive import Recursive

# create an index symbol
n = Symbol('n')

# create the sequence
fib = Recursive('fib')
fib.index = n

# input the first items
fib[0] = 1
fib[1] = 1

# provide a recursive formula
fib[n] = fib[n-1] + fib[n-2]

# resolve it
explicit = fib.resolve()

# test
for i in range(10):
    value = explicit.subs(n, i).simplify()
    print "fib[{}] = {}".format(i, value)


import numpy as np

# Random Number Generator - Uniform Distribution
def U():
        return np.random.rand()

# Integrated Function
def f(x):
    return np.exp(x)

if __name__ == "__main__":
    # Number of Random Numbers
    N = 1000
    # sample_mean(f)
    μf = 0
    # sample_mean(f^2)
    μf2 = 0
    # Inegral computated with Crude-Monte-Carlo Method
    I_sample = 0

    # Limits of Integration
    a, b = 0, 10
    # "Volume" of Integration
    V = (b-a)

    for i in range(N):
        xi = V * U() + a

        μf += f(xi)/N
        μf2 += f(xi)**2/N

    I_sample = V * μf

    # Variance of f(x)
    σf_sample = np.sqrt((N/(N-1)) * (μf2 - μf**2))

    # Integral Error
    δI_sample = V/np.sqrt(N)*σf_sample

    # Integral Relative Error
    η_sample = δI_sample/I_sample

    print(f'I ± δI = {I_sample:.2f} ± {δI_sample:.2f},\nη_sample = {η_sample:.3f}')


import numpy as np

def deriv_fitting_matrix(degree, t_end=1.0):
  """
    Returns A s.t. that the vector x that satisfies

    Ax = b

    contains polynomial coefficients

      p(t) = x_1 + x_2 t + x_3 t^2 ...

    so that

    p(0) = b_1
    p^(i)(0) = b_{i+1}
    p(t_end) = b_{degree / 2}
    p^(i)(t_end) = b_{degree / 2 + i + 1}

    i.e. the first degree / 2 derivatives of p at 0 match the first degree / 2
         entries of b and the first degree / 2 derivatives of p at 1, match the last
         degree / 2 entries of b
  """

  assert degree % 2 == 0

  A = np.zeros((degree, degree))

  ts = t_end ** np.array(range(degree))

  constant_term = 1
  poly = np.ones(degree)
  for i in range(degree // 2):
    A[i, i] = constant_term
    A[degree // 2 + i, :] = np.hstack((np.zeros(i), poly * ts[:degree - i]))
    poly = np.polynomial.polynomial.polyder(poly)
    constant_term *= (i + 1)

  return A

if __name__ == "__main__":
  for i in range(1, 5):
    mat = deriv_fitting_matrix(2 * i)
    print(mat)
    print(np.linalg.inv(mat))


import matplotlib.pyplot as plt
import numpy as np
import scipy.stats as st
import seaborn as sns

sns.set()

def p(x):
    return st.norm.pdf(x, loc=30, scale=10) + st.norm.pdf(x, loc=80, scale=20)

def q(x):
    return st.norm.pdf(x, loc=50, scale=30)

x = np.arange(-50, 151)
k = max(p(x) / q(x))

def rejection_sampling(iter=1000):
    samples = []

    for i in range(iter):
        z = np.random.normal(50, 30)
        u = np.random.uniform(0, k * q(z))

        if u <= p(z):
            samples.append(z)

    return np.array(samples)

if __name__ == "__main__":
    plt.plot(x, p(x))
    plt.plot(x, k * q(x))
    plt.show()

    s = rejection_sampling(iter=100000)
    sns.distplot(s)


import numpy as maths

def lin_reg(*series):
	'''Fits the data (x, y) to the equation y = ax + b and returns (a, b)'''
	if(len(series)) == 1:
		y = series[0]
		x = [index for index in range(len(y))]
	else:
		x, y = series

	x_ = maths.sum(x) / len(x)
	y_ = maths.sum(y) / len(y)
	xy_ = maths.dot(x - x_, y - y_)
	xx_ = maths.dot(x - x_, x - x_)

	a = xy_ / xx_
	b = y_ - (a * x_)
	return(a, b)

def exp_reg(y):
	'''Fits the data (y) to the equation y = b.(a^x) and returns (a, b)'''
	y = [y_ for y_ in y if y_ > 0]			#Clean non-zero values.
	if(len(y) == 0):		#If all entries in y are 0, then the required curve is the x-axis.
		return(0, 0)

	if(len(y) == 1):		#If there's only one entry in y, then the required curve is a line parallel to the x-axis: y = y[0]
		return(y[0], y[0])

	else:
		a, b = maths.exp(lin_reg(maths.log(y)))		#Fit the series to a linear model using a logarithmic scale.
		#y = b.(a^x)		=>		log(y) = x.log(a) + log(b)
		return(a, b)

def exp_predict(x, a, b):
	'''Returns a prediction for an exponenial series characterised by (a, b).'''
	return((a ** x) * b)


import numpy as np
from tqdm.auto import tqdm
from scipy.sparse import coo_matrix
from ortools.linear_solver import pywraplp

__all__ = ["linprog"]

def linprog(f, A, b):
    """
    Solve the following linear programming problem
            maximize_x (f.T).dot(x)
            subject to A.dot(x) <= b
    where   A is a sparse matrix (coo_matrix)
            f is column vector of cost function associated with variable
            b is column vector
    """

    # flatten the variable
    f = f.ravel()
    b = b.ravel()

    solver = pywraplp.Solver('SolveReviewerAssignment',
                             pywraplp.Solver.GLOP_LINEAR_PROGRAMMING)

    infinity = solver.Infinity()
    n, m = A.shape
    x = [[]] * m
    c = [0] * n

    print("Setting up variables...")
    for j in tqdm(range(m)):
        x[j] = solver.NumVar(-infinity, infinity, 'x_%u' % j)

    # state objective function
    print("Setting up objective function...")
    objective = solver.Objective()
    for j in tqdm(range(m)):
        objective.SetCoefficient(x[j], f[j])
    objective.SetMaximization()

    # state the constraints
    print("Setting up constraints...")
    for i in tqdm(range(n)):
        c[i] = solver.Constraint(-infinity, int(b[i]))
        for j in A.col[A.row == i]:
            c[i].SetCoefficient(x[j], A.data[np.logical_and(A.row == i, A.col == j)][0])

    result_status = solver.Solve()
    if result_status != 0:
        print("The final solution might not converged")

    x_sol = np.array([x_tmp.SolutionValue() for x_tmp in x])

    return {'x': x_sol, 'status': result_status}

def test_example():
    """
    Solves example problem from http://www.vitutor.com/alg/linear_programming/example_programming.html
        f(x,y)= 50x + 40y
    subject to:
        2x+3y <= 1500
        2x + y <= 1000
        x >= 0 -> -x <= 0
        y >= 0 -> -y <= 0
    """
    f = np.array([50, 40], dtype=np.float)
    A = np.array([[ 2, 3],
                  [ 2, 1],
                  [-1, 0],
                  [ 0, -1]], dtype=np.float)
    C = np.array([1500, 1000, 0, 0])
    x_sol = linprog(f, coo_matrix(A), C)
    print('Example Problem:')
    print('maximize_x\t 50x + 40y')
    print('s.t.\t\t 2x+3y <= 1500, 2x + y <= 1000, x >= 0, y >= 0')
    print('Solution: (x, y) = ', x_sol)

if __name__ == '__main__':
    test_example()


# global
from typing  import Union, Optional, Tuple, Literal
import jax as _jax
# noinspection PyPackageRequirements
import jaxlib
import jax.numpy as jnp
from jaxlib.xla_extension import Buffer

# local
from ivy import inf

JaxArray = (_jax.interpreters.xla._DeviceArray, jaxlib.xla_extension.DeviceArray, Buffer)

def vector_norm(x: JaxArray,
                p: Union[int, float, Literal[inf, - inf]] = 2,
                axis: Optional[Union[int, Tuple[int]]] = None,
                keepdims: bool = False)\
                    -> JaxArray:

    jnp_normalized_vector = None

    if axis == None:
       jnp_normalized_vector = jnp.linalg.norm(jnp.ravel(x), p, axis, keepdims)

    else:
        jnp_normalized_vector = jnp.linalg.norm(x, p, axis, keepdims)

    if jnp_normalized_vector.shape  == tuple():
        return  jnp.expand_dims(jnp_normalized_vector, 0)
    return jnp_normalized_vector


"""
Convergence comparison of different adaptive filtering algorithms (with
different step sizes) in white Gaussian noise.
"""

import numpy as np
import matplotlib.pyplot as plt
import adaptfilt as adf

# Generating inpud and desired signal
N = 3000
coeffs = np.concatenate(([-4, 3.2], np.zeros(20), [0.7], np.zeros(33), [-0.1]))
u = np.random.randn(N)
d = np.convolve(u, coeffs)

# Perform filtering
M = 60  # No. of taps to estimate
mu1 = 0.0008  # Step size 1 in LMS
mu2 = 0.0004  # Step size 1 in LMS
beta1 = 0.08  # Step size 2 in NLMS and AP
beta2 = 0.04  # Step size 2 in NLMS and AP
K = 3  # Projection order 1 in AP

# LMS
y_lms1, e_lms1, w_lms1 = adf.lms(u, d, M, mu1, returnCoeffs=True)
y_lms2, e_lms2, w_lms2 = adf.lms(u, d, M, mu2, returnCoeffs=True)
mswe_lms1 = adf.mswe(w_lms1, coeffs)
mswe_lms2 = adf.mswe(w_lms2, coeffs)

# NLMS
y_nlms1, e_nlms1, w_nlms1 = adf.nlms(u, d, M, beta1, returnCoeffs=True)
y_nlms2, e_nlms2, w_nlms2 = adf.nlms(u, d, M, beta2, returnCoeffs=True)
mswe_nlms1 = adf.mswe(w_nlms1, coeffs)
mswe_nlms2 = adf.mswe(w_nlms2, coeffs)

# AP
y_ap1, e_ap1, w_ap1 = adf.ap(u, d, M, beta1, K, returnCoeffs=True)
y_ap2, e_ap2, w_ap2 = adf.ap(u, d, M, beta2, K, returnCoeffs=True)
mswe_ap1 = adf.mswe(w_ap1, coeffs)
mswe_ap2 = adf.mswe(w_ap2, coeffs)

# Plot results
plt.figure()
plt.title('Convergence comparison of different adaptive filtering algorithms')
plt.plot(mswe_lms1, 'b', label='LMS with stepsize=%.4f' % mu1)
plt.plot(mswe_lms2, 'b--', label='LMS with stepsize=%.4f' % mu2)
plt.plot(mswe_nlms1, 'g', label='NLMS with stepsize=%.2f' % beta1)
plt.plot(mswe_nlms2, 'g--', label='NLMS with stepsize=%.2f' % beta2)
plt.plot(mswe_ap1, 'r', label='AP with stepsize=%.2f' % beta1)
plt.plot(mswe_ap2, 'r--', label='AP with stepsize=%.2f' % beta2)
plt.legend()
plt.grid()
plt.xlabel('Iterations')
plt.ylabel('Mean-squared weight error')
plt.show()


import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns
import matplotlib.ticker as ticker

# based on
# https://www.wired.com/story/how-fast-does-a-virus-spread/

#########

def InfectionCurve(t, Ninit, loc, scale):

    N = Ninit
    ic = []
    t_old = t[0]

    for it in t:

      t_new = it
      dt = t_new - t_old

      r = np.random.normal(loc=loc, scale=scale, size=None)
      N = N + r * N * dt

      ic.append(N)

      t_old = t_new

      print([r, t_new, dt, N])

    return(ic)

def plt_IC(t, ic, fplot):

    fig, ax = plt.subplots(figsize=(10, 10))

    sns.lineplot(x=t, y=ic, sort=False, linewidth=2)
    sns.scatterplot(t, ic)

    plt.suptitle("Infection curve", fontsize=16, fontweight='bold', color='white')
    plt.xticks(rotation=0)
    plt.xlabel('Time [days]')
    plt.ylabel('Number infected')

    fig.savefig(fplot)
    plt.close()

def plt_IC_n(t, ics, fplot):

    fig, ax = plt.subplots(figsize=(10, 10))

    ax.set(yscale="log")
    ax.yaxis.set_major_formatter(\
            ticker.FuncFormatter(lambda y, _: '{:g}'.format(y)))

    for ic in ics:
        sns.lineplot(x=t, y=ic, sort=False, linewidth=2)
        sns.scatterplot(t, ic)

    plt.suptitle("Infection curve", fontsize=16, fontweight='bold', color='white')
    plt.xticks(rotation=0)
    plt.xlabel('Time [days]')
    plt.ylabel('Number infected')

    fig.savefig(fplot)
    plt.close()

#################

fplot = '../plt/InfectionCurve.png'

# EXPERIMENTO 1
# Como afecta el error en el cálculo de R en la proyección de casos

start = 0.
stop = 40.
num = 41
dt = (stop-start)/num
t = np.linspace(start=start, stop=stop, num=num)

ics = []
for i in range(20):
    ic = InfectionCurve(t, 1., 0.2, 0.05)
    ics.append(ic)

fplot = '../plt/InfectionCurve_sd.png'
plt_IC_n(t, ics, fplot)

# EXPERIMENTO 2
# Como afecta R en el avance de los contagios

R = np.linspace(start=0.15, stop=0.3, num=10)
ics = []
for r in R:
    ic = InfectionCurve(t, 1., r, 0.0)
    ics.append(ic)

fplot = '../plt/InfectionCurve_R.png'
plt_IC_n(t, ics, fplot)

# keep track of:
#
# - numero real de contagiados
# - numero de casos confirmados
# - numero de casos recuerados
# - numero de fallecimientos
# - numero de pacientes leves (en la casa)
# - numero de pacientes moderados (internados, no UTI)
# - numero depacientes graves (UTI)

# infected
# confirmed
# recovered
#
# inf_dead
# inf_uti
# inf_bed
# inf_home
#

def InfectionCurve_full(t, Ninit, loc, scale):

    N = Ninit
    ic = []
    t_old = t[0]

    for it in t:

      t_new = it
      dt = t_new - t_old

      r = np.random.normal(loc=loc, scale=scale, size=None)
      N = N + r * N * dt

      ic.append(N)

      t_old = t_new

      print([r, t_new, dt, N])

    return(ic)


import numpy as np

def reshape_features(X):
    return X.reshape(X.shape[0],-1).T

def initialize_with_zeros(dim):
    w = np.zeros((dim,1))
    b = 0
    return w,b

def compute_loss(Yhat, Y):
    return -(Y * np.log(Yhat) +( (1-Y) * np.log((1-Yhat))))

def compute_cost(Yhat, Y, m):
    loss = compute_loss(Yhat,Y)
    return np.sum(loss) / m

# Calculates sigmoid of z
def sigmoid(z):
    return 1 / (1+np.exp(-z))

def propagate(w, b, X,Y):
    # 1. Forward Propagation
    m = X.shape[1]
    A = sigmoid(w.T @ X)
    cost = compute_cost(A,Y,m)

    # 2. Backward Propagation
    dw = (X @ (A-Y).T)/m
    db = np.sum(A-Y)/m
    cost = np.squeeze(cost)
    grads = {
        'dw' : dw,
        'db' : db
    }

    return grads, cost

def optimize(w, b, X, Y, n_iterations, learning_rate, print_cost=False):
    """
    This function optimizes w and b by running a gradient descent algorithm

    Arguments:
    w -- weights, a numpy array of size (num_px * num_px * 3, 1)
    b -- bias, a scalar
    X -- data of shape (num_px * num_px * 3, number of examples)
    Y -- true "label" vector (containing 0 if non-cat, 1 if cat), of shape (1, number of examples)
    n_iterations -- number of iterations of the optimization loop
    learning_rate -- learning rate of the gradient descent update rule
    print_cost -- True to print the loss every 100 steps

    Returns:
    params -- dictionary containing the weights w and bias b
    grads -- dictionary containing the gradients of the weights and bias with respect to the cost function
    costs -- list of all the costs computed during the optimization, this will be used to plot the learning curve.

    Tips:
    You basically need to write down two steps and iterate through them:
        1) Calculate the cost and the gradient for the current parameters. Use propagate().
        2) Update the parameters using gradient descent rule for w and b.
    """
    costs = []

    for i in range(n_iterations):
        grads, cost = propagate(w,b,X,Y)
        dw = grads['dw']
        db =  grads['db']
        w = w - learning_rate * dw
        b = b - learning_rate * db

        if i % 100 == 0:
            costs.append(cost)

        if print_cost and i % 100 == 0:
            print (f'Cost after iteration {i}: {cost}')

    params = {
        'w' : w,
        'b': b
    }
    grads = {
        'dw': dw,
        'db': db
    }

    return params, grads, costs

def predict(w, b, X):
    '''
    Predict whether the label is 0 or 1 using learned logistic regression parameters (w, b)

    Arguments:
    w -- weights, a numpy array of size (num_px * num_px * 3, 1)
    b -- bias, a scalar
    X -- data of size (num_px * num_px * 3, number of examples)

    Returns:
    Y_prediction -- a numpy array (vector) containing all predictions (0/1) for the examples in X
    '''
    m = X.shape[1]
    Y_prediction = np.zeros((1,m))
    w = w.reshape(X.shape[0], 1)

    # Compute vector "A" predicting the probabilities of a cat being present in the picture
    A = sigmoid(np.dot(w.T,X)+b)

    for i in range(A.shape[1]):
        # Convert probabilities A[0,i] to actual predictions p[0,i]
        Y_prediction[0, i] = 1 if A[0, i] > 0.5 else 0
    assert(Y_prediction.shape == (1, m))
    return Y_prediction

def model(X_train, Y_train, X_test, Y_test, num_iterations = 2000, learning_rate = 0.5, print_cost = False):
    # initialize parameters with zeros (≈ 1 line of code)
    w,b = initialize_with_zeros(X_train.shape[0])

     # Gradient descent
    parameters, grads, costs = optimize(w,b,X_train, Y_train, num_iterations,learning_rate,print_cost)

    # Retrieve parameters w and b from dictionary "parameters"
    w = parameters["w"]
    b = parameters["b"]

    # Predict test/train set examples (≈ 2 lines of code)
    Y_prediction_test = predict(w, b, X_test)
    Y_prediction_train = predict(w, b, X_train)

    # Print train/test Errors
    train_acc = 100 - np.mean(np.abs(Y_prediction_train - Y_train)) * 100
    test_acc = 100 - np.mean(np.abs(Y_prediction_test - Y_test)) * 100
    # print("train accuracy: {} %".format(100 - np.mean(np.abs(Y_prediction_train - Y_train)) * 100))
    # print("test accuracy: {} %".format(100 - np.mean(np.abs(Y_prediction_test - Y_test)) * 100))

    d = {"costs": costs,
         "Y_prediction_test": Y_prediction_test,
         "Y_prediction_train" : Y_prediction_train,
         "w" : w,
         "b" : b,
         "learning_rate" : learning_rate,
         "num_iterations": num_iterations,
         "train_acc": train_acc,
         "test_acc": test_acc
         }

    return d


import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt

def plot_damped_oscillator(m=1.0, k=1.0, b=1.0):
    """
    m = 1.0 # mass [kg]
    k = 1.0 # spring constant [N/m]
    b = 1.0 # friction constant [Ns/m = kgm/s^2*s/m = kg/s]
    """

    two_gamma = b/m
    omega_squared = k/m

    gamma = two_gamma/2
    omega = np.sqrt(omega_squared)

    if gamma == 0.0:
        osc_type = "non damped oscillator"
    elif gamma < omega:
        osc_type = "underdamped"
    elif gamma > omega:
        osc_type = "overdamped"
    else:
        osc_type = "Critically damped"

    def rhs(y, t):
        x, v = y[0], y[1]
        a = -two_gamma*v - omega_squared*x
        y_prime = np.array([v, a])
        return y_prime

    x0 = 10.0 # [m]
    v0 = 0.0 # [m/s]
    y0 = np.array([x0, v0])

    T = 20.0 # [s]
    steps = 1000
    ts = np.linspace(0, T, steps)

    ys = odeint(rhs, y0, ts)
    xs, vs = ys[:,0], ys[:,1]

    plt.subplot(2,1,1)
    plt.title(r"%s $\gamma = %f $, $\omega = %f $" % \
            (osc_type, gamma, omega))
    plt.plot(ts, xs, color="black")
    plt.xlabel("t [s]")
    plt.ylabel("x [m]")
    plt.subplot(2,1,2)
    plt.plot(ts, vs, color="black")
    plt.xlabel("t [s]")
    plt.ylabel("v [m/s]")
    plt.show()

#plot_damped_oscillator()
#plot_damped_oscillator(b=10.0)
# Critically damped
# k = b^2/(4m)
plot_damped_oscillator(k=1/4.)


"""Signal generation utility functions."""

import numpy as np

from vesper.util.bunch import Bunch
import vesper.util.signal_utils as signal_utils

def create_silence(duration, sample_rate):
    length = signal_utils.seconds_to_frames(duration, sample_rate)
    samples = np.zeros((1, length))
    return Bunch(samples=samples, sample_rate=sample_rate)

def add_tone(
        audio, start_time, duration, amplitude, frequency,
        channel_num=0, taper_duration=0):

    fs = audio.sample_rate

    # Create tone.
    length = signal_utils.seconds_to_frames(duration, fs)
    phases = np.arange(length) * 2 * np.pi * frequency / fs
    tone = amplitude * np.sin(phases)

    # Taper ends if specified.
    if taper_duration != 0:
        n = signal_utils.seconds_to_frames(taper_duration, fs)
        ramp = np.arange(n) / n
        tone[:n] *= ramp
        tone[-n:] *= 1 - ramp

    # Add tone to audio.
    start_index = signal_utils.seconds_to_frames(start_time, fs)
    audio.samples[channel_num, start_index:start_index + length] += tone


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

"""Library of computing associated Legendre function of the first kind."""

import math

from jax import lax
import jax.numpy as jnp
import numpy as np

def gen_normalized_legendre(l_max,
                            x):
  r"""Computes the normalized associated Legendre functions (ALFs).

  The ALFs of the first kind are used in spherical harmonics. The spherical
  harmonic of degree `l` and order `m` can be written as
  `Y_l^m(θ, φ) = N_l^m * P_l^m(cos(θ)) * exp(i m φ)`, where `N_l^m` is the
  normalization factor and θ and φ are the colatitude and longitude,
  repectively. `N_l^m` is chosen in the way that the spherical harmonics form
  a set of orthonormal basis function of L^2(S^2). For the computational
  efficiency of spherical harmonics transform, the normalization factor is
  embedded into the computation of the ALFs. In addition, normalizing `P_l^m`
  avoids overflow/underflow and achieves better numerical stability. Three
  recurrence relations are used in the computation. Note that the factor of
  \sqrt(1 / (4 𝛑)) is used in the formulation.

  Args:
    l_max: The maximum degree of the associated Legendre function. Both the
      degrees and orders are `[0, 1, 2, ..., l_max]`.
    x: A vector of type `float32`, `float64` containing the sampled points in
      spherical coordinates, at which the ALFs are computed; `x` is essentially
      `cos(θ)`.

  Returns:
    The 3D array of shape `(l_max + 1, l_max + 1, len(x))` containing the
    normalized values of the ALFs at `x`.
  """
  dtype = lax.dtype(x)
  if dtype not in (jnp.float32, jnp.float64):
    raise TypeError(
        'x.dtype={} is not supported, see docstring for supported types.'
        .format(dtype))

  if x.ndim != 1:
    raise ValueError('x must be a 1D array.')

  p = np.zeros((l_max + 1, l_max + 1, x.shape[0]))

  # The initial value p(0,0).
  initial_value = 0.5 / np.sqrt(math.pi)
  p[0, 0] = initial_value

  # Compute the diagonal entries p(l,l) with recurrence.
  y = np.sqrt(1.0 - x * x)
  for l in range(1, l_max + 1):
    a = -1.0 * np.sqrt(1.0 + 0.5 / l)
    p[l, l] = a * y * p[l - 1, l - 1]

  # Compute the off-diagonal entries with recurrence.
  for l in range(l_max):
    b = np.sqrt(2.0 * l + 3.0)
    p[l + 1, l] = b * x * p[l, l]

  # Compute the remaining entries with recurrence.
  for m in range(l_max + 1):
    for l in range(m + 2, l_max + 1):
      c0 = l * l
      c1 = m * m
      c2 = 2.0 * l
      c3 = (l - 1.0) * (l - 1.0)
      d0 = np.sqrt((4.0 * c0 - 1.0) / (c0 - c1))
      d1 = np.sqrt(((c2 + 1.0) * (c3 - c1)) / ((c2 - 3.0) * (c0 - c1)))
      p[l, m] = d0 * x * p[l - 1, m] - d1 * p[l - 2, m]

  return jnp.asarray(p)