import numpy as np
from scipy.signal import welch
from scipy.integrate import simps
from matplotlib import pyplot as plt
from scipy import signal
import numpy.fft as fft
from sklearn.preprocessing import StandardScaler

class features:
    def __init__(self, X, Y):
        self.X = X.copy()
        self.Y = Y.copy()

        self.features = []
        self.labels = Y.copy()

        self.num_channels = self.X.shape[1]
        self.num_trials = self.X.shape[0]
        self.size_trials = self.X.shape[2]

    def getDatos(self):
        return self.features, self.labels

    def calc_hjorth(self, x):
        first_deriv = np.diff(x)
        second_deriv= np.diff(x, 2)
        var_zero = np.mean(x**2)
        var_d1 = np.mean(first_deriv**2)
        var_d2 = np.mean(second_deriv**2)
        activity = var_zero
        mobility = np.sqrt(var_d1/var_zero)
        complexity = np.sqrt(var_d2/var_d1) / mobility

        return activity, mobility, complexity

    def _embed(self, x, order=3, delay=1):
        """Time-delay embedding.
        Parameters
        ----------
        x : 1d-array
            Time series, of shape (n_times)
        order : int
            Embedding dimension (order).
        delay : int
            Delay.
        Returns
        -------
        embedded : ndarray
            Embedded time-series, of shape (n_times - (order - 1) * delay, order)
        """
        N = len(x)
        if order * delay > N:
            raise ValueError("Error: order * delay should be lower than x.size")
        if delay < 1:
            raise ValueError("Delay has to be at least 1.")
        if order < 2:
            raise ValueError("Order has to be at least 2.")
        Y = np.zeros((order, N - (order - 1) * delay))
        for i in range(order):
            Y[i] = x[(i * delay):(i * delay + Y.shape[1])]
        return Y.T

    def perm_entropy(self, x, order=3, delay=1, normalize=False):
        '''Permutation Entropy.
        Parameters
        ----------
        x : list or np.array
            One-dimensional time series of shape (n_times)
        order : int
            Order of permutation entropy. Default is 3.
        delay : int
            Time delay (lag). Default is 1.
        normalize : bool
            If True, divide by log2(order!) to normalize the entropy between 0
            and 1. Otherwise, return the permutation entropy in bit.
        Returns
        -------
        pe : float
            Permutation Entropy.
        Notes
        -----
        The permutation entropy is a complexity measure for time-series first
        introduced by Bandt and Pompe in 2002.
        The permutation entropy of a signal :math:`x` is defined as:
        .. math:: H = -\\sum p(\\pi)\\log_2(\\pi)
        where the sum runs over all :math:`n!` permutations :math:`\\pi` of order
        :math:`n`. This is the information contained in comparing :math:`n`
        consecutive values of the time series. It is clear that
        :math:`0 ≤ H (n) ≤ \\log_2(n!)` where the lower bound is attained for an
        increasing or decreasing sequence of values, and the upper bound for a
        completely random system where all :math:`n!` possible permutations appear
        with the same probability.'''

        from math import factorial, log
        x = np.array(x)
        ran_order = range(order)
        hashmult = np.power(order, ran_order)
        # Embed x and sort the order of permutations
        sorted_idx = self._embed(x, order=order, delay=delay).argsort(kind='quicksort')
        # Associate unique integer to each permutations
        hashval = (np.multiply(sorted_idx, hashmult)).sum(1)
        # Return the counts
        _, c = np.unique(hashval, return_counts=True)
        # Use np.true_divide for Python 2 compatibility
        p = np.true_divide(c, c.sum())
        pe = -np.multiply(p, np.log2(p)).sum()
        if normalize:
            pe /= np.log2(factorial(order))
        return pe

    def permutation_entropy(self):
        features = []
        for trial in self.X:
            feature = []
            for ch in range(trial.shape[0]):
                segmento = trial[ch,:]
                pe = self.perm_entropy(segmento)
                feature.extend([pe])

            features.append(feature)

        self.features = np.asarray(features).copy()

    def calc_higuchi(self, a, k_max = 5):
        L = []
        x = []
        N = a.size

        for k in range(1,k_max):
            Lk = 0
            for m in range(0,k):
                #we pregenerate all idxs
                idxs = np.arange(1,int(np.floor((N-m)/k)),dtype=np.int32)

                Lmk = np.sum(np.abs(a[m+idxs*k] - a[m+k*(idxs-1)]))
                Lmk = (Lmk*(N - 1)/(((N - m)/ k)* k)) / k
                Lk += Lmk

        L.append(np.log(Lk/(m+1)))
        x.append([np.log(1.0/ k), 1])

        (p, r1, r2, s)=np.linalg.lstsq(x, L)
        return p[0]

    def hjorth(self):
        features = []
        for trial in self.X:
            feature = []
            for ch in range(trial.shape[0]):
                segmento = trial[ch,:]
                activity, mobility, complexity = self.calc_hjorth(segmento)

                feature.extend([activity, mobility, complexity])

            features.append(feature)

        self.features = np.asarray(features).copy()

    def statistics_features(self, mean_b, std_b, max_b, min_b, kurtosis_b, wave_b):
        from scipy.stats import kurtosis

        features = []
        for trial in self.X:
            feature = []
            for ch in range(trial.shape[0]):
                segmento = trial[ch,:]
                f = []
                if mean_b == 1:
                    a = np.mean(segmento)
                    f.append(a)
                if std_b == 1:
                    a = np.std(segmento)
                    f.append(a)
                if max_b == 1:
                    a = max(segmento)
                    f.append(a)
                if min_b == 1:
                    a = min(segmento)
                    f.append(a)
                if kurtosis_b == 1:
                    a = kurtosis(segmento)
                    f.append(a)
                if wave_b == 1:
                    a = self.wave_length(segmento)
                    f.append(a)

                f = np.asarray(f)

                feature.extend(f)

            features.append(feature)

        self.features = np.asarray(features).copy()

    def butter_bandpass(self, lowcut, highcut, fm = 250, order=5):
        from scipy.signal import butter, welch, filtfilt, lfilter
        from scipy import signal

        nyq = 0.5 * fm
        low = lowcut / nyq
        high = highcut / nyq
        b, a = butter(order, [low, high], btype='band')

        return b, a

    def hjorth_freq(self, fm = 250):
        from scipy.signal import butter, welch, filtfilt, lfilter
        from scipy import signal

        features = []
        for trial in self.X:
            feature = []
            for ch in range(trial.shape[0]):
                segmento = trial[ch,:]

                b1, a1 = self.butter_bandpass(1, 4, fm, 5)
                b2, a2 = self.butter_bandpass(4, 7, fm, 5)
                b3, a3 = self.butter_bandpass(8, 12, fm, 5)
                b4, a4 = self.butter_bandpass(13, 30, fm, 5)
                b5, a5 = self.butter_bandpass(31, 45, fm, 5)

                channel = np.asarray(np.concatenate((segmento, segmento)))
                tamano = channel.shape[0]

                y1 = lfilter(b1, a1, channel)
                y1 = y1[0:tamano]

                y2 = lfilter(b2, a2, channel)
                y2 = y2[0:tamano]

                y3 = lfilter(b3, a3, channel)
                y3 = y3[0:tamano]

                y4 = lfilter(b4, a4, channel)
                y4 = y4[0:tamano]

                y5 = lfilter(b5, a5, channel)
                y5 = y5[0:tamano]

                activity1, mobility1, complexity1 = self.calc_hjorth(y1)
                activity2, mobility2, complexity2 = self.calc_hjorth(y2)
                activity3, mobility3, complexity3 = self.calc_hjorth(y3)
                activity4, mobility4, complexity4 = self.calc_hjorth(y4)
                activity5, mobility5, complexity5 = self.calc_hjorth(y5)

                f = [activity1, mobility1, complexity1, activity2, mobility2, complexity2,
                                activity3, mobility3, complexity3, activity4, mobility4, complexity4,
                                activity5, mobility5, complexity5]
                f = (f-np.min(f))/(np.max(f)-np.min(f)) #Normalizamos las features
                feature.extend(f)

            features.append(feature)

        self.features = np.asarray(features).copy()

    def higuchi(self, k=5):
        features = []
        for trial in self.X:
            feature = []
            for ch in range(trial.shape[0]):
                segmento = trial[ch,:]
                activity = self.calc_higuchi(segmento, k)
                feature.extend([activity])

            features.append(feature)

        self.features = np.asarray(features).copy()

    def CPT5(sef, x):
        den = len(x)*np.exp(np.std(x))
        return sum(np.exp(x))/den

    def SSC(self, x):
        x = np.array(x)
        x = np.append(x[-1], x)
        x = np.append(x,x[1])
        xn = x[1:len(x)-1]
        xn_i2 = x[2:len(x)]    # xn+1
        xn_i1 = x[0:len(x)-2]  # xn-1
        ans = np.heaviside((xn-xn_i1)*(xn-xn_i2),0)
        return sum(ans[1:])

    def wave_length(self, x):
        x = np.array(x)
        x = np.append(x[-1], x)
        x = np.append(x,x[1])
        xn = x[1:len(x)-1]
        xn_i2 = x[2:len(x)]    # xn+1
        return sum(abs(xn_i2-xn))

    def SRAV(self, x):
        SRA = sum(np.sqrt(abs(x)))
        return np.power(SRA/len(x),2)

    def mean_abs(self, x):
        return sum(abs(x))/len(x)

    def statics(self,):
        from scipy.stats import kurtosis

        features = []
        for trial in self.X:
            feature = []
            for ch in range(trial.shape[0]):
                segmento = trial[ch,:]
                segmento = StandardScaler().fit_transform(segmento.reshape(-1,1)).reshape(1,-1)[0]
                p1 =  self.CPT5(segmento)
                p2 = self.SSC(segmento)
                p3 = self.wave_length(segmento)
                p4 = self.SRAV(segmento)
                p5 = self.mean_abs(segmento)

                feature.extend([p1,p2,p3,p4,p5])

            features.append(feature)

        self.features = np.asarray(features).copy()

    def statics_hjorth(self,):
        features = []
        for trial in self.X:
            feature = []
            for ch in range(trial.shape[0]):
                segmento = trial[ch,:]
                p1 = np.mean(segmento)
                p2 = np.std(segmento)
                p3 = max(segmento)
                p4 = min(segmento)
                p5 = sum(segmento)
                activity, mobility, complexity = self.calc_hjorth(segmento)

                feature.extend([p1, p2, p3, p4, p5,  activity, mobility, complexity])

            features.append(feature)

        self.features = np.asarray(features).copy()

    def calculate_entropy(self, list_values):
        counter_values = Counter(list_values).most_common()
        probabilities = [elem[1]/len(list_values) for elem in counter_values]
        entropy=scipy.stats.entropy(probabilities)
        return entropy

    def calculate_statistics(self, list_values):
        n5 = np.nanpercentile(list_values, 5)
        n25 = np.nanpercentile(list_values, 25)
        n75 = np.nanpercentile(list_values, 75)
        n95 = np.nanpercentile(list_values, 95)
        median = np.nanpercentile(list_values, 50)
        mean = np.nanmean(list_values)
        std = np.nanstd(list_values)
        var = np.nanvar(list_values)
        rms = np.nanmean(np.sqrt(list_values**2))
        # New features
        kur = kurtosis(list_values)
        MeanAbs = mean_abs(list_values)
        norm_ent = norm_entropy(list_values)
        skewness = skew(list_values)
        CPT_5 = CPT5(list_values)
        SSC_1 = SSC(list_values)
        WL = wave_length(list_values)
        SRAV_1 = SRAV(list_values)
        return [n5, n25, n75, n95, median, mean, std, var, rms, kur, MeanAbs, norm_ent, skewness, CPT_5, SSC_1, WL, SRAV_1]

    def calculate_crossings(self, list_values):
        zero_crossing_indices = np.nonzero(np.diff(np.array(list_values) > 0))[0]
        no_zero_crossings = len(zero_crossing_indices)
        mean_crossing_indices = np.nonzero(np.diff(np.array(list_values) > np.nanmean(list_values)))[0]
        no_mean_crossings = len(mean_crossing_indices)
        return [no_zero_crossings, no_mean_crossings]

    def get_features(self, list_values):
        '''entropy = self.calculate_entropy(list_values)
        crossings = self.calculate_crossings(list_values)'''
        statistics = self.calculate_statistics(list_values)
        return statistics
        #return [entropy] + crossings + statistics

    def wavelet(self, level):
        import pywt

        features = []
        for trial in self.X:
            feature = []
            for ch in range(trial.shape[0]):
                segmento = trial[ch,:]

                data_std = StandardScaler().fit_transform(segmento.reshape(-1,1)).reshape(1,-1)[0]

                # WPD tree
                wptree = pywt.WaveletPacket(data=data_std, wavelet='db5', mode='symmetric', maxlevel=level)
                levels = wptree.get_level(level, order = "freq")

                #Feature extraction for each node
                for node in levels:
                    data_wp = node.data
                    '''activity, mobility, complexity = self.calc_hjorth(data_wp)
                    feature.extend([activity, mobility, complexity])'''
                    # Features group
                    feature.extend(self.get_features(data_wp))

            features.append(feature)

        self.features = np.asarray(features).copy()

    def bandpower(self, data, band, fm, window_sec=None, relative=False):
        """Compute the average power of the signal x in a specific frequency band.

        Parameters
        ----------
        data : 1d-array
            Input signal in the time-domain.
        sf : float
            Sampling frequency of the data.
        band : list
            Lower and upper frequencies of the band of interest.
        window_sec : float
            Length of each window in seconds.
            If None, window_sec = (1 / min(band)) * 2
        relative : boolean
            If True, return the relative power (= divided by the total power of the signal).
            If False (default), return the absolute power.

        Return
        ------
        bp : float
            Absolute or relative band power.
        """

        band = np.asarray(band)
        low, high = band

        # Define window length
        if window_sec is not None:
            nperseg = window_sec * fm
        else:
            nperseg = (2 / low) * fm

        # Compute the modified periodogram (Welch)
        freqs, psd = welch(data, fm, nperseg=nperseg)

        # Frequency resolution
        freq_res = freqs[1] - freqs[0]

        # Find closest indices of band in frequency vector
        idx_band = np.logical_and(freqs >= low, freqs <= high)

        # Integral approximation of the spectrum using Simpson's rule.
        bp = simps(psd[idx_band], dx=freq_res)

        if relative:
            bp /= simps(psd, dx=freq_res)

        return bp

    def BP(self, banda1, banda2, fm):
        features = []
        for trial in self.X:
            feature = []
            for ch in range(trial.shape[0]):
                segmento = trial[ch,:]
                b = self.bandpower(segmento,[banda1,banda2],fm)
                feature.extend([b])

            features.append(feature)

        self.features = np.asarray(features).copy()

    def psd(self, fm):
        features = []
        for trial in self.X:
            feature = []
            for ch in range(trial.shape[0]):
                segmento = trial[ch,:]
                freqs, psd = welch(segmento, fm, nperseg=125)

                idx_band = np.logical_and(freqs >= 1, freqs <= 25)
                feature.extend(psd[idx_band])

            features.append(feature)

        self.features = np.asarray(features).copy()

    def bands_alpha_beta(self, fm):
        from scipy.fft import fft, ifft

        features = []
        for trial in self.X:
            feature = []
            for ch in range(trial.shape[0]):
                segmento = trial[ch,:]

                fft_wave  = np.fft.fft(segmento)
                fft_fre = np.fft.fftfreq(n=segmento.size, d=1/fm)
                #Cojo los valores de 8-12 Hz, 13-24 Hz, 25-30 Hz

                idx_band1 = np.logical_and(fft_fre >= 8, fft_fre <= 12)
                idx_band2 = np.logical_and(fft_fre >= 13, fft_fre <= 24)
                idx_band3 = np.logical_and(fft_fre >= 25, fft_fre <= 30)

                alpha = sum(fft_wave.real[idx_band1])
                beta1 = sum(fft_wave.real[idx_band2])
                beta2 = sum(fft_wave.real[idx_band3])

                feature.extend([alpha, beta1, beta2])

                '''plt.subplot(211)
                plt.plot(fft_fre, fft_wave.real, label="Real part")
                plt.xlim(-50,50)
                plt.ylim(-600,600)
                plt.legend(loc=1)
                plt.title("FFT in Frequency Domain")

                plt.subplot(212)
                plt.plot(fft_fre, fft_wave.imag,label="Imaginary part")
                plt.legend(loc=1)
                plt.xlim(-50,50)
                plt.ylim(-600,600)
                plt.xlabel("frequency (Hz)")

                plt.show()'''

            features.append(feature)

        self.features = np.asarray(features).copy()

    def calculate_entropy(self,list_values):
        from collections import Counter
        import scipy

        counter_values = Counter(list_values).most_common()
        probabilities = [elem[1]/len(list_values) for elem in counter_values]
        entropy=scipy.stats.entropy(probabilities)
        return entropy

    def calculate_statistics(self,list_values):
        n5 = np.nanpercentile(list_values, 5)
        n25 = np.nanpercentile(list_values, 25)
        n75 = np.nanpercentile(list_values, 75)
        n95 = np.nanpercentile(list_values, 95)
        median = np.nanpercentile(list_values, 50)
        mean = np.nanmean(list_values)
        std = np.nanstd(list_values)
        var = np.nanvar(list_values)
        rms = np.nanmean(np.sqrt(list_values**2))
        return [n5, n25, n75, n95, median, mean, std, var, rms]

    def get_features(self,list_values):
        #entropy = self.calculate_entropy(list_values)
        statistics = self.calculate_statistics(list_values)
        return statistics #[entropy] + statistics

    def entropy_statics(self):
        from sklearn import preprocessing
        import pywt

        features = []
        for trial in self.X:
            feature = []
            for ch in range(trial.shape[0]):
                segmento = trial[ch,:]
                #[cfs, _] = pywt.cwt(segmento, scales, waveletname, 1)
                #power = (abs(cfs)) ** 2

                feature += self.get_features(segmento)
                #print(np.asarray(feature).shape)

                '''power = (abs(cfs)) ** 2

                period = 1. / frequencies
                levels = [0.0625, 0.125, 0.25, 0.5, 1, 2, 4, 8]
                f, ax = plt.subplots(figsize=(15, 10))
                ax.contourf(time, np.log2(period), np.log2(power), np.log2(levels),
                            extend='both')

                ax.set_title('%s Wavelet Power Spectrum (%s)' % ('Nino1+2', waveletname))
                ax.set_ylabel('Period (seconds)')
                Yticks = 2 ** np.arange(np.ceil(np.log2(period.min())),
                                        np.ceil(np.log2(period.max())))
                ax.set_yticks(np.log2(Yticks))
                ax.set_yticklabels(Yticks)
                ax.invert_yaxis()
                ylim = ax.get_ylim()
                ax.set_ylim(ylim[0], -1)

                plt.show()'''

            features.append(feature)

        self.features = np.asarray(features).copy()

    def create_cwt_images(self, n_scales, rescale_size, wavelet_name = "morl"):
        import pywt

        n_samples = self.X.shape[0]
        n_signals = self.X.shape[1]

        X_cwt = np.ndarray(shape=(n_samples, rescale_size, rescale_size, n_signals), dtype = 'float32')

        scales = np.arange(1, n_scales + 1)
        for sample in range(n_samples):
            for ch in range(self.X[sample].shape[0]):
                segmento = self.X[sample,ch,:]
                coeffs, _ = pywt.cwt(segmento, scales, wavelet_name)
                rescale_coeff = np.resize(coeffs,(rescale_size, rescale_size))
                X_cwt[sample,:,:,ch] = rescale_coeff

        self.features = X_cwt.copy()

    def create_time_images(self, fm):
        import math
        import pywt

        n_samples = self.X.shape[0]
        n_signals = self.X.shape[1]

        rescale_size = int(math.sqrt(fm))
        X_cwt = np.ndarray(shape=(n_samples, rescale_size, rescale_size, n_signals), dtype = 'float32')

        for sample in range(n_samples):
            for ch in range(self.X[sample].shape[0]):
                segmento = self.X[sample,ch,:]
                segmento = np.resize(segmento,(rescale_size, rescale_size))
                X_cwt[sample,:,:,ch] = segmento

        self.features = X_cwt.copy()

    def create_STFT(self):
        import math
        import pywt

        n = 16

        features = []
        for sample in range(len(self.X)):
            features_segment = []
            for ch in range(self.X[sample].shape[0]):
                segmento = self.X[sample,ch,:]
                s = self.stft(segmento, n, Nfft=n*2)
                t, f = self.stftbins(segmento, n, Nfft=n*2, d=1 / 250)
                assert (len(t) == s.shape[0])
                assert (len(f) == s.shape[1])

                '''plt.imshow(np.abs(s), aspect="auto", extent=[f[0], f[-1], t[-1], t[0]])
                plt.xlabel('frequency (Hertz)')
                plt.ylabel('time (seconds (start of chunk))')
                plt.title('STFT with chirp example')
                plt.grid()
                plt.show()'''

                s = np.abs(s)
                s = np.asarray(s)
                feature = np.reshape(s,(s.shape[0]*s.shape[1]))
                features_segment.extend(feature)

            features.append(features_segment)

        self.features = np.asarray(features)
            #self.features[sample,:,:,ch] = f

    def stft(self, x, Nwin, Nfft=None):
        """
        Short-time Fourier transform: convert a 1D vector to a 2D array
        The short-time Fourier transform (STFT) breaks a long vector into disjoint
        chunks (no overlap) and runs an FFT (Fast Fourier Transform) on each chunk.
        The resulting 2D array can
        Parameters
        ----------
        x : array_like
            Input signal (expected to be real)
        Nwin : int
            Length of each window (chunk of the signal). Should be ≪ `len(x)`.
        Nfft : int, optional
            Zero-pad each chunk to this length before FFT. Should be ≥ `Nwin`,
            (usually with small prime factors, for fastest FFT). Default: `Nwin`.
        Returns
        -------
        out : complex ndarray
            `len(x) // Nwin` by `Nfft` complex array representing the STFT of `x`.

        See also
        --------
        istft : inverse function (convert a STFT array back to a data vector)
        stftbins : time and frequency bins corresponding to `out`
        """
        Nfft = Nfft or Nwin
        Nwindows = x.size // Nwin
        # reshape into array `Nwin` wide, and as tall as possible. This is
        # optimized for C-order (row-major) layouts.
        arr = np.reshape(x[:Nwindows * Nwin], (-1, Nwin))
        stft = fft.rfft(arr, Nfft)
        return stft

    def stftbins(self, x, Nwin, Nfft=None, d=1.0):
        """
        Time and frequency bins corresponding to short-time Fourier transform.
        Call this with the same arguments as `stft`, plus one extra argument: `d`
        sample spacing, to get the time and frequency axes that the output of
        `stft` correspond to.
        Parameters
        ----------
        x : array_like
            same as `stft`
        Nwin : int
            same as `stft`
        Nfft : int, optional
            same as `stft`
        d : float, optional
            Sample spacing of `x` (or 1 / sample frequency), units of seconds.
            Default: 1.0.
        Returns
        -------
        t : ndarray
            Array of length `len(x) // Nwin`, in units of seconds, corresponding to
            the first dimension (height) of the output of `stft`.
        f : ndarray
            Array of length `Nfft`, in units of Hertz, corresponding to the second
            dimension (width) of the output of `stft`.
        """
        Nfft = Nfft or Nwin
        Nwindows = x.size // Nwin
        t = np.arange(Nwindows) * (Nwin * d)
        f = fft.rfftfreq(Nfft, d)
        return t, f


import numpy as np
import newton_raphson as nr

# Creates a gravitational force function
def gravitational_force(k, z0):
    return lambda z: -k * (z - z0) / (((z-z0)**2).sum()**(3/2))

# Creates the jacobian matix correspounding to a gravitational force
def gravitional_jacob(k, z0):
    def res(z):
        v = ((z[0] - z0[0])**2 + (z[1] - z[1])**2 )**(3/2)
        x = z[0]
        y = z[1]
        x0 = z0[0]
        y0 = z0[1]
        resx_x = -k * (v - (x - x0) *(3/2) * ((x-x0)**2 + (y - y0)**2)**(1/2) * 2 * (x - x0))/ (v ** 2)
        resx_y = -k * ( - (x - x0) *(3/2) * ((x-x0)**2 + (y - y0)**2)**(1/2) * 2 * (y - y0 ))/ (v ** 2)
        resy_x = -k * ( - (y - y0) *(3/2) * ((x-x0)**2 + (y - y0)**2)**(1/2) * 2 * (x - x0 ))/ (v ** 2)
        resy_y = -k * (v - (y - y0) *(3/2) * ((x-x0)**2 + (y - y0)**2)**(1/2) * 2 * (y - y0))/ (v ** 2)
        return np.matrix([[resx_x,resx_y],[resy_x, resy_y]])
    return res;

# Creates a centrifigual force function
def centrifigual_force(k, z0):
    return lambda z : k * (z - z0)

# Creates the jacobian matix correspounding to a centrifigual force
def centrifigual_jacob(k, z0):
    return lambda z : np.matrix([[k,0],[0,k]])

# Creates an elastic force function
def elastic_force(k, z0):
    return lambda z: - k * (z - z0)

# Creates the jacobian matix correspounding to an elastic force
def elastic_jacob(k, z0):
    return lambda z : np.matrix([[-k,0],[0,-k]])

# Apply two gravitational forces to z
def forces1(z):
    return gravitational_force(1,np.array([0,0]))(z) + gravitational_force(0.01, np.array([1,0]))(z)

# Apply two gravitational forces and a centrifigual force to z
def forces2(z):
    return forces1(z) + centrifigual_force(1, np.array([0.01/1.01, 0]))(z)

# Produces the jacobian matix of two gravitational forces aplied to z
def jacobians1(z):
    return gravitional_jacob(1,np.array([0,0]))(z) + gravitional_jacob(0.01, np.array([1,0]))(z)

# Produces the jacobian matix of two gravitational forces and a centrifigual force aplied to z
def jacobians2(z):
    return jacobians1(z) + centrifigual_jacob(1, np.array([0.01/1.01, 0]))(z)

# Finds an equilibrium point for forces1 and forces2
def solve_two_grav():
    print("First case : Two gravitational forces with respective coefficients 1 (resp. 0.01) originating from [0,0] (resp. [1,0])")
    Eq1 = nr.Newton_Raphson(forces1, jacobians1, np.array([1.5,0]), 1000, 1e-10);
    print("Equilibrium points =", Eq1)
    print("f(Eq) =", forces1(Eq1))

    print("Second case : previous forces and a centrifugal force centered on the barycenter of the two masses, with coefficient 1")
    Eq2 = nr.Newton_Raphson(forces2, jacobians2, np.array([1.5,0]), 1000, 1e-10);
    print("Equilibrium points =", Eq2)
    print("f(Eq) =", forces2(Eq2))

# Tests forces using an example
def test_forces():
    U = np.array([1.5,0])
    print("U =", U)
    print("f(U) =", forces2(U))
    print("df(U) =\n", jacobians2(U))

# Finds an equilibrium point for forces2 depending on U0
def find_lagragian_points(U0):
    # U0 = (0.5,  0)  -> U = ( 0.85927766, 0 ) = L1
    # U0 = (1.5,  0)  -> U = ( 1.15775715, 0 ) = L2
    # U0 = (-1.5, 0)  -> U = (-0.99754112, 0 ) = L3

    U = nr.Newton_Raphson(forces2, jacobians2, U0, 1000, 1e-10)
    print("U =", U)
    print("f(U) =", forces2(U))
    print("df(U) =\n", jacobians2(U))

if __name__ == "__main__":
    test_forces()
    solve_two_grav()


import numpy as np

class mdp:
    def __init__(self, grid_map, state_trans_prob=1, discount_factor=0.95):
        # self.
        self.state_space = grid_map.state_space    # width and height
        self.action_space = grid_map.action_space  # up, down, left, right

        self.state_trans_prob = state_trans_prob
        self.gamma = discount_factor  # the discount_factor to calculate the action value

        self.policy_matrix = np.ones((grid_map.state_space[0], grid_map.state_space[1], len(self.action_space))) / len(self.action_space)  # policy matrix, include the probability of each action, the init probabilities are equal

        self.grid_map = grid_map

    def policy_iteration(self, policy_value):
        ## please complete this function by 1. calculating the action value 2. finding the action index with maximum value

        # (1) The policy iteration under current policy value
        # (2) In each state index, the action value will be calculated depend on current policy value and action reward (the formula in the lecture)
        # (3) In each state index, find the action with maximum value calculated by previous line (2)., check whether it is equal to the old action
        # (4) If all the actions are stable, the policy iteration is done.

        interate_done = True

        for i in range(self.state_space[0]):
            for j in range (self.state_space[1]):

                old_action_index = np.argmax(self.policy_matrix[i, j])
                action_value_list = []
                state_index = (i, j)

                for action_index, action in enumerate(self.action_space):
                    next_state, reward, state_prob, done = self.grid_map.step(state_index, action_index)

                    # calculate the action value
                    # action_value = ?
                    action_value_list.append(action_value)

                # find the action index with maximum action value
                # action_index = ?

                self.policy_matrix[i, j, :] = 0
                self.policy_matrix[i, j, action_index] = 1

                # iteration done

                if action_index != old_action_index:
                    interate_done = False

        return interate_done

    def value_iteration(self, threshold=0.01):

        ## please complete this function by 1. calculating the policy value 2. completing the judgement condition indicator delta
        # (1) The value iteration under current policy matrix
        # (2) in each state index, the policy value will be calculated (cumulative reward and value)
        # (3) This iteration will be done if all the values are stable (Compare the maximum difference value with the threshold to judge)

        policy_value = np.zeros((self.state_space[0], self.state_space[1]))

        while True:
            delta = 0

            for i in range (self.state_space[0]):
                for j in range (self.state_space[1]):

                    old_value = policy_value[i, j]

                    state_index = (i, j)
                    temp_action_value = 0

                    for action_index, action_prob in enumerate(self.policy_matrix[i, j]):
                        next_state_index, reward, state_prob, done = self.grid_map.step( state_index, action_index)

                        ##  calculate the temp_action_value (policy value)
                        # temp_action_value ?

                    policy_value[i, j] = temp_action_value

                    ## complete the judgement condition indicator  (Compare the maximum difference value with the threshold to judge)
                    # delta = ?

            if delta < threshold:
                break

        return policy_value


# -*- coding: utf-8 -*-
"""
Quadratic primes
================
https://projecteuler.net/problem=27

Euler discovered the remarkable quadratic formula:

    n^2 + n + 41

It turns out that the formula will produce 40 primes for the consecutive integer
values 0 <= n <= 39. However, when n = 40, 40^2 + 40 + 41 = 40(40 + 1) +
41 is divisible by 41, and certainly when n = 41, 41^2 + 41 + 41 is clearly
divisible by 41.

The incredible formula n^2 - 79n + 1601 was discovered, which produces 80
primes for the consecutive values 0 <= n <= 79. The product of the
coefficients, −79 and 1601, is −126479.

Considering quadratics of the form:

    n^2 + an + b,

    where |a| < 1000 and |b| <= 1000
    where |n| is the absolute value of n

Find the product of the coefficients, a and b, for the quadratic expression
that produces the maximum number of primes for consecutive values of n,
starting with n = 0.
"""
import functools

import numpy as np

from .utils import prime_mask, print_result

def func(n: int, a: int, b: int) -> int:
    return n ** 2 + a * n + b

@functools.lru_cache
def primes() -> np.ndarray:
    return prime_mask(func(1000, 1000, 1000))

@functools.lru_cache()
def depth(a: int, b: int) -> int:
    """Return the number of consecutive N that produce a prime for func(n, a, b)."""
    n = 0
    while primes()[func(n, a, b)]:
        n += 1
    return n

@print_result
def solve() -> int:
    d_max = 0
    best = None
    for b in range(-999, 1001, 2):
        # B must be prime to satisfy f(n=0)
        if not primes()[b]:
            continue
        for a in range(-999, 1000):
            if depth(a, b) > d_max:
                best = (a, b)
                d_max = depth(a, b)
    if best is None:
        raise RuntimeError("Failed to find solution")
    return best[0] * best[1]

if __name__ == "__main__":
    solve()


import sys
import numpy as np

if "../" not in sys.path:
    sys.path.append('../')
np.random.seed(10)
from env.gridWorld import gridWorld

# Problem: Evaluate a given policy pi
# Solution: Iterative application of Bellman expectation backup

def policy_evaluation(policy, env, discount_factor=1.0, threshold=0.00001):
    """
    Evaluate a policy given an environment anda full description of the
    environment's dynamics.

    Args:
        policy: [S, A] shaped matrix representing the policy.
        env: OpenAI env. env.P represents the transition probabilities of the environment.
             env.nS is the number of states in the environment.
             env.nA is the number of actions in the environment.
        theta: We stop evaluation once our value function change is less than theta for all states.
        discount_factor: Gamma discount factor

    Returns:
        Vector of legnth env.nS representing the value function.
    """

    # Start with a all 0 value function
    V = np.zeros(env.nS)
    while True:
        delta = 0
        # for each state perform a full backup
        for state in range(env.nS):
            v = 0
            # Look at each possiple next action
            for action, action_prob in enumerate(policy[state]):
                # For each action, look at the possible next state
                for prob, next_state, reward, done in env.P[state][action]:
                    # Calculate the expected value. Ref: Sutton book eq. 4.6.
                    v += action_prob * prob * (reward + discount_factor * V[next_state])
            # How much our value function changed (across any states)
            delta = max(delta, np.abs(v - V[state]))
            V[state] = v
        if delta < threshold:
            break
    return np.round(np.array(V), 1)

if __name__ == "__main__":

    env = gridWorld()

    random_policy = np.ones([env.nS, env.nA]) / env.nA

    V = policy_evaluation(random_policy, env)

    print(f"Value Function:\n{V.reshape(env.shape)}")


"""
Component to demonstrate using an ImplicitComponent to perform a polynomial curve fit.
"""
import openmdao.api as om
import numpy as np

class PolynomialFit(om.ImplicitComponent):

    def initialize(self):
        self.options.declare('N_cp', types=int)
        self.options.declare('N_predict', types=int)

    def setup(self):

        # data to fit, which can also be thought of as the "control points"
        # of the fit function
        self.add_input('x_cp', shape=self.options['N_cp'])
        self.add_input('y_cp', shape=self.options['N_cp'])

        # location of the points you want to evaluate the final fit function at
        self.add_input('x', shape=self.options['N_predict'])
        # computed value of the fitted polynomial at the x points
        self.add_output('y', shape=self.options['N_predict'])

        # these are the coefficients of the polynomial function you are fitting
        self.add_output('A', np.zeros(6))  # assuming a 5th order polynomial

        # analytic derivatives are left as an exercise
        # using CS here will give accurate partials, but will miss the sparsity pattern
        self.declare_partials('*', '*', method='cs')

    def apply_nonlinear(self, inputs, outputs, residuals):

        a0, a1, a2, a3, a4, a5 = outputs['A']
        X_cp = inputs['x_cp']
        Y_cp = inputs['y_cp']

        Y_computed = a0 + a1*X_cp + a2*X_cp**2 + a3*X_cp**3 + a4*X_cp**4 + a5*X_cp**5

        # error = np.sum((Y_computed-Y_cp)**2)

        # note that derivatives are showing up in the apply_nonlinear method because
        # this is the formulation we use to form the residual.
        # We are minimizing the sum of the square of the error: np.sum((Y_computed-Y_cp)**2) w.r.t A
        # hence we differentiate the objective w.r.t A and set the resulting system of equations to 0
        d_error__d_Y_computed = 2*(Y_computed-Y_cp)

        d_Y_computed__d_a0 = np.ones(self.options['N_cp'])
        d_Y_computed__d_a1 = X_cp
        d_Y_computed__d_a2 = X_cp**2
        d_Y_computed__d_a3 = X_cp**3
        d_Y_computed__d_a4 = X_cp**4
        d_Y_computed__d_a5 = X_cp**5

        residuals['A'][0] = np.sum(d_error__d_Y_computed * d_Y_computed__d_a0)
        residuals['A'][1] = np.sum(d_error__d_Y_computed * d_Y_computed__d_a1)
        residuals['A'][2] = np.sum(d_error__d_Y_computed * d_Y_computed__d_a2)
        residuals['A'][3] = np.sum(d_error__d_Y_computed * d_Y_computed__d_a3)
        residuals['A'][4] = np.sum(d_error__d_Y_computed * d_Y_computed__d_a4)
        residuals['A'][5] = np.sum(d_error__d_Y_computed * d_Y_computed__d_a5)

        X = inputs['x']
        Y = a0 + a1*X + a2*X**2 + a3*X**3 + a4*X**4 + a5*X**5
        residuals['y'] = Y - outputs['y']


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Fri Sep 20 15:53:35 2019

@author: Ary
"""

import numpy as np
import pandas as pd
import sklearn.linear_model as skl
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error
import math
from sklearn.model_selection import KFold
from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_val_score
from sklearn.preprocessing import MinMaxScaler
from sklearn.svm import SVR

from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter

np.random.seed(2204)

## part a
def FrankeFunction(x,y):
    term1 = 0.75*np.exp(-(0.25*(9*x-2)**2) - 0.25*((9*y-2)**2))
    term2 = 0.75*np.exp(-((9*x+1)**2)/49.0 - 0.1*(9*y+1))
    term3 = 0.5*np.exp(-(9*x-7)**2/4.0 - 0.25*((9*y-3)**2))
    term4 = -0.2*np.exp(-(9*x-4)**2 - (9*y-7)**2)
    return term1 + term2 + term3 + term4

def Design_Matrix_X(x, y, n):
	N = len(x)
	l = int((n+1)*(n+2)/2)
	X = np.ones((N,l))

	for i in range(1,n+1):
		q = int((i)*(i+1)/2)
		for k in range(i+1):
			X[:,q+k] = x**(i-k) * y**k

	return X

n_x=1000
m=5

x = np.random.uniform(0, 1, n_x)
y = np.random.uniform(0, 1, n_x)

z = FrankeFunction(x, y)

#print(x)

n = int(len(x))
z_1 = z +0.01*np.random.randn(n)

X= Design_Matrix_X(x,y,n=m)
DesignMatrix = pd.DataFrame(X)
#print(DesignMatrix)

a = np.linalg.matrix_rank(X) #we check it is not a singular matrix
#print(a)

beta = np.linalg.inv(X.T.dot(X)).dot(X.T).dot(z_1)
ztilde = X @ beta
#print(beta)

beta1 = skl.LinearRegression().fit(X,z_1) #function .fit fits linear models
ztilde1 = beta1.predict(X)

#print(ztilde)
#print('--')
#print(ztilde1)

var_beta_OLS = 1*np.linalg.inv(X.T.dot(X))
var = pd.DataFrame(var_beta_OLS)
#print(var)
var_diag=np.diag(var_beta_OLS)
#print(var_diag)

l1_OLS = beta - 1.96*np.sqrt(var_diag)/(X.shape[0])
l2_OLS = beta + 1.96*np.sqrt(var_diag)/(X.shape[0])
#print(l1_OLS)
#print(l2_OLS)

def MSE (ydata, ymodel):
    n = np.size(ymodel)
    y = (ydata - ymodel).T@(ydata - ymodel)
    y = y/n
    return y

def R2 (ydata, ymodel):
   return 1-((ydata-ymodel).T@(ydata-ymodel))/((ydata-np.mean(ydata)).T@(ydata-np.mean(ydata)))

print(MSE(z_1,ztilde))
print(R2(z_1,ztilde))

print("Mean squared error: %.2f" % mean_squared_error(z_1, ztilde))
print('Variance score: %.2f' % r2_score(z_1, ztilde))

## part b

def train_test_splitdata(x_,y_,z_,i):

	x_learn=np.delete(x_,i)
	y_learn=np.delete(y_,i)
	z_learn=np.delete(z_,i)
	x_test=np.take(x_,i)
	y_test=np.take(y_,i)
	z_test=np.take(z_,i)

	return x_learn,y_learn,z_learn,x_test,y_test,z_test

def k_fold(k,x,y,z,m,model):
    n=len(x)
    j=np.arange(n)
    np.random.shuffle(j)
    n_k=int(n/k)
    MSE_K_t = 0
    R2_K_t = 0
    Variance_t=0
    Bias_t=0
    betas = np.zeros((k,int((m+1)*(m+2)/2)))
    z_pred = np.zeros((200,k))
    z_test1 = np.zeros((200,k))
    z_train1 = np.zeros((800,k))
    z_pred_train = np.zeros((800,k))
    for i in range(k):
        x_l,y_l,z_l,x_test,y_test,z_test=train_test_splitdata(x,y,z,j[i*n_k:(i+1)*n_k])
        z_test1[:,i]=z_test
        z_train1[:,i]=z_l
        X = Design_Matrix_X(x_l,y_l,m)
        X_test= Design_Matrix_X(x_test,y_test,m)
        #print(pd.DataFrame(X))
        #print(pd.DataFrame(X_test))
        beta1= model.fit(X,z_l)
        beta = beta1.coef_
        print(beta[0])
        betas[i] = beta
        ztilde1 = beta1.predict(X_test)
        ztilde_l = beta1.predict(X)
        #print(ztilde1)
        z_pred[:,i] = ztilde1
        z_pred_train[:,i] = ztilde_l
       # MSE_K_t+=MSE(z_test,ztilde1)
        R2_K_t+=R2(z_test,ztilde1)
       # Bias_t+=bias(z_test,ztilde1)
       # Variance_t+=variance(ztilde1)
# check if the values computed with our function and using the methods in lines 161-163 are the same
    #error_t = MSE_K_t/k
    #bias_t = Bias_t/k
    #variance_t = Variance_t/k
    R2_t = R2_K_t/k
    #print(error_t)
    #print(bias_t)
    #print(variance_t)

    error_test = np.mean(np.mean((z_test1 - z_pred)**2 , axis=1, keepdims=True))
    bias___ = np.mean( (z_test1 - np.mean(z_pred, axis=1, keepdims=True))**2 )
    variance___ = np.mean( (z_pred - np.mean(z_pred, axis=1, keepdims=True))**2 )
    error_train = np.mean(np.mean((z_train1 - z_pred_train)**2 , axis=1, keepdims=True))

    return (error_test, bias___,variance___ , error_train, R2_t, np.std(betas, axis = 0), np.mean(betas, axis = 0))

def variance(y_tilde):
	return np.sum((y_tilde - np.mean(y_tilde))**2)/np.size(y_tilde)

def bias(y, y_tilde):
	return np.sum((y - np.mean(y_tilde))**2)/np.size(y_tilde)

a=k_fold(5,x,y,z_1,5,LinearRegression(fit_intercept=False))
error_test = a[0]
bias___ = a[1]
variance___ = a[2]
error_train = a[3]
print('{} = {} + {}= {}'.format(error_test, bias___, variance___, bias___+variance___))

print('BBB')
from sklearn import model_selection
from sklearn.linear_model import LinearRegression
kfold = model_selection.KFold(n_splits=5, shuffle=True)
X= Design_Matrix_X(x,y,n=5)
k=5
z_pred = []
z_test1 = []
z_train1 = []
z_pred_train = []
for train_index, test_index in kfold.split(X):
    print("TRAIN:", train_index, "TEST:", test_index)
    X_train, X_test = X[train_index], X[test_index]
    z_train, z_test = z[train_index], z[test_index]
    z_test1.append(z_test)
    z_train1.append(z_train)
    print(X_train.shape, X_test.shape)
    model = LinearRegression(fit_intercept=False)
    model.fit(X_train,z_train)
    z_pred.append(model.predict(X_test))
    z_pred_train.append(model.predict(X_train))
bias = np.mean( (z_test - np.mean(z_pred))**2 )
variance = np.mean( (z_pred - np.mean(z_pred))**2 )
mse = model_selection.cross_val_score(model, X, z_1, cv=kfold, scoring='neg_mean_squared_error')
r2 = model_selection.cross_val_score(model, X, z_1, cv=kfold, scoring='r2')
print(bias)
print(variance)
print(np.absolute(mse.mean()))
print(r2.mean())

# part c

maxdegree = 20

def fold_degree(maxdegree,x,y,z,k):
    error__t = np.zeros(maxdegree)
    bias__t = np.zeros(maxdegree)
    variance__t = np.zeros(maxdegree)
    polydegree = np.zeros(maxdegree)
    var_score__t = np.zeros(maxdegree)
    error__l = np.zeros(maxdegree)
    for degree in range(maxdegree):
        #z_pred = np.empty((2000, k))
        degree_fold = k_fold(k, x, y, z, degree, LinearRegression())
        error_t = degree_fold[0]
        bias_t = degree_fold[1]
        variance_t = degree_fold[2]
        var_score_t = degree_fold[4]
        error_l = degree_fold[3]
        polydegree[degree] = degree
        error__t[degree] = error_t
        bias__t[degree] = bias_t
        variance__t[degree] = variance_t
        var_score__t[degree] = var_score_t
        error__l[degree] = error_l
        print(degree)
        print(error_t)
        print(variance_t)
    return (polydegree, error__t, bias__t, variance__t, var_score__t, error__l)

b = fold_degree(maxdegree, x, y, z, 5)
#print(b[1])
#print(b[2], b[3])
#print(b[1]+b[3])

plt.plot(b[0], (b[1]), label='Error')
plt.plot(b[0], (b[2]), label='bias')
plt.plot(b[0], (b[3]), label='Variance')
plt.legend()
plt.show()

plt.plot(b[0], (b[1]), label='Error test')
plt.plot(b[0], (b[5]), label='Error learning')
plt.legend()
plt.show()

from sklearn.utils import resample

n_boostraps = 100

error_test = np.zeros(maxdegree)
bias___ = np.zeros(maxdegree)
variance___ = np.zeros(maxdegree)
polydegree = np.zeros(maxdegree)
error_train = np.zeros(maxdegree)
x_train, x_test, y_train, y_test, z_train, z_test = train_test_split(x, y, z, test_size=0.2, shuffle=True)
z_test1 = np.zeros((200,100))
z_train1 = np.zeros((800,100))
for i in range(100):
    z_test1[:,i]=z_test

for degree in range(maxdegree):
    model = LinearRegression(fit_intercept=False)
    z_pred = np.empty((z_test.shape[0],n_boostraps))
    z_pred_train = np.empty((z_train.shape[0],n_boostraps))
    for i in range(n_boostraps):
        x_, y_, z_ = resample(x_train, y_train, z_train)
        z_train1[:,i] = z_
        X_train = Design_Matrix_X(x_,y_,degree)
        X_test= Design_Matrix_X(x_test,y_test,degree)
        z_pred[:, i] = model.fit(X_train, z_).predict(X_test).ravel()
        z_pred_train[:, i] = model.fit(X_train, z_).predict(X_train).ravel()

    polydegree[degree] = degree
    error_test[degree] = np.mean(np.mean((z_test1 - z_pred)**2 , axis=1, keepdims=True))
    bias___[degree] = np.mean( (z_test1 - np.mean(z_pred, axis=1, keepdims=True))**2 )
    variance___[degree] = np.mean( np.var(z_pred, axis=1, keepdims=True))
    error_train[degree] = np.mean(np.mean((z_train1 - z_pred_train)**2 , axis=1, keepdims=True))
    #print(degree)
    #print(error_test)
    #print(bias___)
    #print(variance___)
    #print(bias___+variance___)

plt.plot(polydegree, error_test, label='Error')
plt.plot(polydegree, bias___, label='bias')
plt.plot(polydegree, variance___, label='Variance')
plt.legend()
plt.show()

plt.plot(polydegree, error_test, label='Error test')
plt.plot(polydegree, error_train, label='error training')
plt.legend()
plt.show()

#part d

lamdas = [0.001, 0.01, 0.1, 1]

for lamda in lamdas:
    beta_r = np.linalg.inv(X.T.dot(X)+lamda*np.identity(21)).dot(X.T).dot(z_1)
    zridge = X @ beta_r
    print("Beta parameters")
    print(beta_r)
#print(zridge)

    clf_ridge = skl.Ridge(alpha=lamda).fit(X, z_1)
    zridge1 = clf_ridge.predict(X)
#print(zridge1)

    M = np.linalg.inv(X.T.dot(X)+lamda*np.identity(21))
    var_beta_ridge = M.dot(X.T).dot(X).dot(M.T)
    var_b_ridge = np.diag(var_beta_ridge)
    print("Variance of betas")
    print(var_b_ridge)

    l1_Ridge = beta_r - 1.96*np.sqrt(var_b_ridge)/(X.shape[0])
    l2_Ridge = beta_r + 1.96*np.sqrt(var_b_ridge)/(X.shape[0])
#print(l1_Ridge)
#print(l2_Ridge)

    print(MSE(z_1,zridge))
    print(R2(z_1,zridge))

    c = k_fold(5,x,y,z,5,skl.Ridge(alpha=lamda))
#print(c[0])
#print(c[1])
#print(c[2])
#print(c[3])

def fold_degree_r(x,y,z,k,lamdas):
    error = np.zeros(len(lamdas))
    bias = np.zeros(len(lamdas))
    variance = np.zeros(len(lamdas))
    polylamda = np.zeros(len(lamdas))
    for lamda in lamdas:
        lamda_fold = k_fold(k, x, y, z, 5, skl.Ridge(alpha=lamda))
        error_ = lamda_fold[0]
        bias_ = lamda_fold[2]
        #print(bias_)
        variance_ = lamda_fold[3]
       # print('AAA')
        #print(lamdas.index(lamda))
        polylamda[lamdas.index(lamda)] = lamda
        error[lamdas.index(lamda)] = error_
        bias[lamdas.index(lamda)] = bias_
        variance[lamdas.index(lamda)] = variance_
    return (polylamda, error, bias, variance)

d = fold_degree_r(x, y, z, 5, lamdas)
#print(b[2])

plt.plot(d[0], d[1], label='Error')
plt.plot(d[0], d[2], label='bias')
plt.plot(d[0], d[3], label='Variance')
plt.legend()
plt.show()

n_boostraps = 100

error_test = np.zeros(len(lamdas))
bias___ = np.zeros(len(lamdas))
variance___ = np.zeros(len(lamdas))
polylamda = np.zeros(len(lamdas))
error_train = np.zeros(len(lamdas))
x_train, x_test, y_train, y_test, z_train, z_test = train_test_split(x, y, z, test_size=0.2, shuffle=True)
z_test1 = np.zeros((200,100))
z_train1 = np.zeros((800,100))
for i in range(100):
    z_test1[:,i]=z_test

for lamda in lamdas:
    model = skl.Ridge(alpha=lamda)
    z_pred = np.empty((z_test.shape[0],n_boostraps))
    z_pred_train = np.empty((z_train.shape[0],n_boostraps))
    for i in range(n_boostraps):
        x_, y_, z_ = resample(x_train, y_train, z_train)
        z_train1[:,i] = z_
        X_train = Design_Matrix_X(x_,y_,5)
        X_test= Design_Matrix_X(x_test,y_test,5)
        z_pred[:, i] = model.fit(X_train, z_).predict(X_test).ravel()
        z_pred_train[:, i] = model.fit(X_train, z_).predict(X_train).ravel()

    polylamda[lamdas.index(lamda)] = lamda
    error_test[lamdas.index(lamda)] = np.mean(np.mean((z_test1 - z_pred)**2 , axis=1, keepdims=True))
    bias___[lamdas.index(lamda)] = np.mean( (z_test1 - np.mean(z_pred, axis=1, keepdims=True))**2 )
    variance___[lamdas.index(lamda)] = np.mean( np.var(z_pred, axis=1, keepdims=True))
    error_train[lamdas.index(lamda)] = np.mean(np.mean((z_train1 - z_pred_train)**2 , axis=1, keepdims=True))
    print(lamda)
    print(error_test)
    print(bias___)
    print(variance___)
    print(bias___+variance___)

plt.plot(lamdas, error_test, label='Error')
plt.plot(lamdas, bias___, label='bias')
plt.plot(lamdas, variance___, label='Variance')
plt.legend()
plt.show()

plt.plot(lamdas, error_test, label='Error test')
plt.plot(lamdas, error_train, label='error training')
plt.legend()
plt.show()

# part e)

lamda=0.01
model_lasso = skl.Lasso(alpha=lamda).fit(X, z_1)
betas = model_lasso.coef_
zlasso = model_lasso.predict(X)
print(MSE(z_1,zlasso))
print(R2(z_1,zlasso))

e = k_fold(5,x,y,z,5,skl.Lasso(alpha=lamda))
print(e[0])

lamdas = [0.001, 0.01, 0.1, 1]

def fold_degree_r(x,y,z,k):
    lamdas = [0.001, 0.01, 0.1, 1]
    error = np.zeros(len(lamdas))
    bias = np.zeros(len(lamdas))
    variance = np.zeros(len(lamdas))
    polylamda = np.zeros(len(lamdas))
    for lamda in lamdas:
        lamda_fold = k_fold(k, x, y, z, 5, skl.Lasso(alpha=lamda))
        error_ = lamda_fold[0]
        bias_ = lamda_fold[2]
        #print(bias_)
        variance_ = lamda_fold[3]
       # print('AAA')
        #print(lamdas.index(lamda))
        polylamda[lamdas.index(lamda)] = lamda
        error[lamdas.index(lamda)] = error_
        bias[lamdas.index(lamda)] = bias_
        variance[lamdas.index(lamda)] = variance_
    return (polylamda, error, bias, variance)

f = fold_degree_r(x, y, z, 5)
print(f[1], f[2])

plt.plot(f[0], f[1], label='Error')
plt.plot(f[0], f[2], label='bias')
plt.plot(f[0], f[3], label='Variance')
plt.legend()
plt.show()

n_boostraps = 100

error_test = np.zeros(len(lamdas))
bias___ = np.zeros(len(lamdas))
variance___ = np.zeros(len(lamdas))
polylamda = np.zeros(len(lamdas))
error_train = np.zeros(len(lamdas))
x_train, x_test, y_train, y_test, z_train, z_test = train_test_split(x, y, z, test_size=0.2, shuffle=True)
z_test1 = np.zeros((200,100))
z_train1 = np.zeros((800,100))
for i in range(100):
    z_test1[:,i]=z_test

for lamda in lamdas:
    model = skl.Lasso(alpha=lamda)
    z_pred = np.empty((z_test.shape[0],n_boostraps))
    z_pred_train = np.empty((z_train.shape[0],n_boostraps))
    for i in range(n_boostraps):
        x_, y_, z_ = resample(x_train, y_train, z_train)
        z_train1[:,i] = z_
        X_train = Design_Matrix_X(x_,y_,5)
        X_test= Design_Matrix_X(x_test,y_test,5)
        z_pred[:, i] = model.fit(X_train, z_).predict(X_test).ravel()
        z_pred_train[:, i] = model.fit(X_train, z_).predict(X_train).ravel()

    polylamda[lamdas.index(lamda)] = lamda
    error_test[lamdas.index(lamda)] = np.mean(np.mean((z_test1 - z_pred)**2 , axis=1, keepdims=True))
    bias___[lamdas.index(lamda)] = np.mean( (z_test1 - np.mean(z_pred, axis=1, keepdims=True))**2 )
    variance___[lamdas.index(lamda)] = np.mean( np.var(z_pred, axis=1, keepdims=True))
    error_train[lamdas.index(lamda)] = np.mean(np.mean((z_train1 - z_pred_train)**2 , axis=1, keepdims=True))
    print(lamda)
    print(error_test)
    print(bias___)
    print(variance___)
    print(bias___+variance___)

plt.plot(error_test, label='Error')
plt.semilogx(lamdas, error_test)
print(lamdas)
print(error_test)
plt.xlabel('lamdas')
plt.plot(lamdas, bias___, label='bias')
plt.plot(lamdas, variance___, label='Variance')
plt.legend()
plt.show()

plt.plot(lamdas, error_test, label='Error test')
plt.plot(lamdas, error_train, label='error training')
plt.legend()
plt.show()


# author : Etienne THIERY

import numpy, random
import cholesky, matgen
import matplotlib.pyplot as plt
import timeit

def test_cholesky():
    random.seed()
    for i in range(10):
        print(".", end="", flush= True)
        size = random.randint(100, 200)
        M = matgen.symmetricPositiveDefinite(size)
        T = cholesky.completeCholesky(M)
        if not numpy.allclose(M, numpy.dot(T, T.transpose())):
            return False
    return True

def testIncompleteCholeskyPrecision():
    size = 100
    nbOfPoints = 100
    x, y = [], []
    for i in numpy.linspace(0, size*(size-1), nbOfPoints):
        x.append(i/(size*size))
        M = matgen.symmetricSparsePositiveDefinite(size, i)
        complete = cholesky.completeCholesky(M)
        incomplete = cholesky.incompleteCholesky(M)
        y.append(numpy.linalg.norm(complete-incomplete)/numpy.linalg.norm(complete))
    plt.plot(x, y, marker='o')
    plt.title("Relative error made by Cholesky incomplete factorization, in function of matrix density\n")
    plt.ylabel("relative error on the norm of the matrix obtained")
    plt.xlabel("density of the initial 100*100 matrix")
    plt.show()

def wrapper(func, *args, **kwargs):
    def wrapped():
        return func(*args, **kwargs)
    return wrapped

def compareCompleteVSIncomplete():
    size = 100
    nbOfPoints = 50
    nbValuesForAverage = 4
    x, yComplete, yIncomplete = [], [], []

    for i in numpy.linspace(0, size*(size-1), nbOfPoints):
        # Computes matrix density
        x.append((size*size-i)/(size*size))
        # Computes average execution times on 3 calls on different matrix
        completeTime, incompleteTime = 0, 0
        for j in range(nbValuesForAverage):
            M = matgen.symmetricSparsePositiveDefinite(size, i)
            wrapped1 = wrapper(cholesky.completeCholesky, M)
            completeTime += timeit.timeit(wrapped1, number=1)
            wrapped2 = wrapper(cholesky.incompleteCholesky, M)
            incompleteTime += timeit.timeit(wrapped2, number=1)
        yComplete.append(completeTime/nbValuesForAverage)
        yIncomplete.append(incompleteTime/nbValuesForAverage)

    p1 = plt.plot(x, yComplete, 'b', marker='o')
    p2 = plt.plot(x, yIncomplete, 'g', marker='o')
    plt.title("Average execution time for the Cholesky factorization in function of matrix density\n")
    plt.legend(["New custom Complete factorization", "Custom incomplete factorization"], loc=4)
    plt.ylabel("Execution time (s)")
    plt.xlabel("density of the initial 100*100 matrix")
    plt.show()

def compareCholeskyCustomVSNumpy():
    maxSize = 500
    nbOfPoints = 200
    nbValuesForAverage = 2
    x, yCustom, yNumpy = [], [], []

    for size in [round(i) for i in numpy.linspace(5, maxSize, nbOfPoints)]:
        # Computes matrix density
        x.append(size)
        # Computes average execution times on nbValuesForAverage calls
        # on different matrix
        customTime, numpyTime = 0, 0
        for j in range(nbValuesForAverage):
            M = matgen.symmetricPositiveDefinite(size)
            wrapped1 = wrapper(cholesky.completeCholesky, M)
            customTime += timeit.timeit(wrapped1, number=1)
            wrapped2 = wrapper(numpy.linalg.cholesky, M)
            numpyTime += timeit.timeit(wrapped2, number=1)
        yCustom.append(customTime/nbValuesForAverage)
        yNumpy.append(numpyTime/nbValuesForAverage)

    p1 = plt.plot(x, yCustom, 'b', marker='o')
    p2 = plt.plot(x, yNumpy, 'g', marker='o')
    plt.title("Average execution time for the Cholesky factorization (custom and Numpy's) in function of size\n")
    plt.legend(["New custom Cholesky factorization", "Numpy Cholesky factorization"], loc=2)
    plt.ylabel("Execution time (s)")
    plt.xlabel("size")
    plt.show()

def printTest(test_func):
    print("Testing " + test_func.__name__[5:] + " : ", end="", flush=True)
    print(("" if test_func() else "un") + "expected behaviour", flush=True)

printTest(test_cholesky)
print("Testing the precision of my custom Incomplete Cholesky Factorization")
testIncompleteCholeskyPrecision()
print("Comparing the execution times of my custom Cholesky Factorizations")
compareCompleteVSIncomplete()
print("Comparing the execution times of my custom Cholkesy Factorization and Numpy's")
compareCholeskyCustomVSNumpy()


#!/usr/bin/env python
"""
Utilities for beam transformation
"""
from __future__ import division
import numpy as np

def rotationMatrix3D(rotaxis, theta):
    """
    Generates a 3D rotation of angle $\theta$ about the specified axis

    rotaxis - vector (x, y, z) specifying the axis of rotation
    theta - angle (in radians) of rotation
    """
    # print("In rotationMarix3D(): rotaxis={}, theta={}".format(rotaxis, theta))
    assert np.ndim(rotaxis) == np.ndim(theta)+1 == 1, "Dimension mismatch in rotationMatrix3D()"
    norm = np.linalg.norm(rotaxis)
    assert norm > 0, "Rotation axis has norm of zero"
    rotaxis = np.array(rotaxis) / norm
    ux = rotaxis[0]
    uy = rotaxis[1]
    uz = rotaxis[2]
    costh = np.cos(theta)
    sinth = np.sin(theta)

    R = np.matrix('{} {} {}; {} {} {}; {} {} {}'.format(
        costh+ux**2 * (1 - costh), ux*uy*(1-costh) - uz*sinth, ux*uz*(1-costh) + uy*sinth,
        uy*ux*(1-costh) + uz*sinth, costh + uy**2 * (1-costh), uy*uz*(1-costh) - ux*sinth,
        uz*ux*(1-costh) - uy*sinth, uz*uy * (1-costh) + ux*sinth, costh + uz**2 * (1-costh)
    ))

    return R

def rotateVec(vec, rotaxis, theta):
    """
    Given a 3-vector vec, rotate about rotaxis by $\theta$

    Also accepts iterable input for vec and rotaxis if the arguments are
    compatible lengths.
    """
    assert not (np.any(np.isnan(vec)) or
                np.any(np.isnan(rotaxis)) or
                np.any(np.isnan(theta))), "Inputs must not be NaN"
    if np.shape(vec) == (3, ):
        R = rotationMatrix3D(rotaxis, theta)
        norm = np.linalg.norm(vec)
        res = np.dot(R, vec)
        assert np.isclose(np.linalg.norm(res), norm), "Rotation changed vector norm"
        return np.dot(R, vec)
    else:
        assert np.shape(vec)[0] == np.shape(rotaxis)[0] == np.shape(theta)[0], "Dimension mismatch in rotateVec()"
        # Unfortunately, seems that np.dot can't be coerced into doing this operation all at once
        # Tried to build a tensor of rotation matrices and use np.einsum, but couldn't get good reuslts.
        # If this becomes slow at any point, it's a good target for optimization.
        res = np.zeros(shape=(np.shape(vec)[0], 3))
        for i, (v, r, t) in enumerate(zip(vec, rotaxis, theta)):
            # print("In rotateVec(): r={}, t={}".format(r, t))
            norm = np.linalg.norm(v)
            R = rotationMatrix3D(r, t)
            res[i] = np.dot(R, v)
            assert np.isclose(np.linalg.norm(res[i]), norm), "Rotation changed vector norm: v={}, r={}, t={}, R={}".format(v, r, t, R)
        return np.hsplit(res, 3)

def vector_rotation_test():
    assert np.allclose(rotateVec((1, 0, 0), (0, 0, 1), np.pi/2.), (0, 1, 0))
    assert np.allclose(rotateVec((1, 1, 0), (0, 0, 1), np.pi/4.), (0, np.sqrt(2), 0))

def multiple_vector_test():
    vecs = [(1, 0, 0),
            (0, 1, 0),
            (0, 0, 1)]
    rotaxes = [(0, 0, 1),
               (0, 0, 1),
               (0, 0, 1)]
    thetas = [np.pi/4,
              np.pi/4,
              np.pi/4]
    expected = np.hsplit(np.array([(np.sqrt(2)/2., np.sqrt(2)/2., 0),
                                   (-np.sqrt(2)/2., np.sqrt(2)/2., 0),
                                   (0, 0, 1)]), 3)
    res = rotateVec(vecs, rotaxes, thetas)
    assert np.allclose(res, expected)


import numpy as np

from scipy.special import comb

from .derivative import register

@register()
def finitediff(x, y, **kwargs):
    dx = x[1] - x[0]
    dy = np.zeros_like(x)
    dy[1:-1] = (y[2:] - y[:-2]) / (2.0 * dx)

    dy[0] = (-3.0 / 2 * y[0] + 2 * y[1] - y[2] / 2) / dx
    dy[-1] = (3.0 / 2 * y[-1] - 2 * y[-2] + y[-3] / 2) / dx

    return dy

@register()
def holoborodko(x, y, M=2):
    """

    https://github.com/jslavin/holoborodko_diff

    Implementation of Pavel Holoborodko's method of "Smooth noise-robust
    differentiators" see
    http://www.holoborodko.com/pavel/numerical-methods/numerical-derivative/
    smooth-low-noise-differentiators
    Creates a numerical approximation to the first derivative of a function
    defined by data points.  End point approximations are found from
    approximations of lower order.  Greater smoothing is achieved by using a
    larger value for the order parameter, M.
    Parameters
    ----------
    x : float array or scalar
        abscissa values of function or, if scalar, uniform step size
    y : float array
        ordinate values of function (same length as x if x is an array)
    M : int, optional (default = 2)
        order for the differentiator - will use surrounding 2*M + 1 points in
        creating the approximation to the derivative
    Returns
    -------
    dydx : float array
        numerical derivative of the function of same size as y
    """

    def coeffs(M):
        """
        Generate the "Smooth noise-robust differentiators" as defined in Pavel
        Holoborodko's formula for c_k
        Parameters
        ----------
        M : int
            the order of the differentiator
        c : float array of length M
            coefficents for k = 1 to M
        """
        m = (2 * M - 2) / 2
        k = np.arange(1, M + 1)
        c = 1.0 / 2.0 ** (2 * m + 1) * (comb(2 * m, m - k + 1) - comb(2 * m, m - k - 1))
        return c

    if np.isscalar(x):
        x = x * np.arange(len(y))

    N = 2 * M + 1
    m = (N - 3) / 2
    c = coeffs(M)
    df = np.zeros_like(y)
    nf = len(y)
    fk = np.zeros((M, (nf - 2 * M)))

    for i, cc in enumerate(c):
        # k runs from 1 to M
        k = i + 1
        ill = M - k
        ilr = M + k
        iul = -M - k
        # this formulation is needed for the case the k = M, where the desired
        # index is the last one -- but range must be given as [-2*M:None] to
        # include that last point
        iur = (-M + k) or None
        fk[i, :] = 2.0 * k * cc * (y[ilr:iur] - y[ill:iul]) / (x[ilr:iur] - x[ill:iul])

    df[M:-M] = fk.sum(axis=0)
    # may want to incorporate a variety of methods for getting edge values,
    # e.g. setting them to 0 or just using closest value with M of the ends.
    # For now we recursively calculate values closer to the edge with
    # progressively lower order approximations -- which is in some sense
    # ideal, though maybe not for all cases
    if M > 1:
        dflo = holoborodko(x[: 2 * M], y[: 2 * M], M=M - 1)
        dfhi = holoborodko(x[-2 * M :], y[-2 * M :], M=M - 1)
        df[:M] = dflo[:M]
        df[-M:] = dfhi[-M:]
    else:
        df[0] = (y[1] - y[0]) / (x[1] - x[0])
        df[-1] = (y[-1] - y[-2]) / (x[-1] - x[-2])
    return df

# from finitediff import interpolate_by_finite_diff as ifd
#
# def finitediff(t, x, order=2, n_points=1, n_fit_ratio=10):
#     """Compute time-derivative of the data matrix X along first axis.
#     Args:
#         t : shape(n_samples)
#         x : array-like, shape (n_samples, n_features)
#             Input variables to be derived.
#     """
#
#     t_fit = np.linspace(t[0], t[-1], num=len(t) * n_fit_ratio, endpoint=True)
#
#     t_fit[0] = t_fit[0] + (t_fit[1] - t_fit[0]) / 2
#     t_fit[-1] = t_fit[-2] + (t_fit[-1] - t_fit[-2]) / 2
#     return ifd(t, x, t_fit, order, n_points, n_points)[::n_fit_ratio, ..., 1:]


import numpy as np

# create vector using numpy
# 1D array is the vector
# create python list and convert into array
x = [1,2,3]
vector = np.array(x)
# array([1, 2, 3])

vector.ndim
# 1 --> 1D

vector.shape
# (3,) --> 1D array

# ARTHIMETIC OPERATIONS ON VECTORS {element wise operation}

# Addition

# create "a" 1D vector between 10 to 50 with size = 5
# vector in theory, array in pratical representation
a = np.random.randint(10,50,size=5)
# print a --> array([29, 10, 30, 21, 41])

# create "b" 1D vector between 25 to 75 with size = 5
b = np.random.randint(20,75,size=5)
# print b --> array([28, 54, 22, 54, 71])

# vection addition
c = a + b
# print c --> array([ 57,  64,  52,  75, 112])

# vection subtraction
d = a - b
# print d --> array([  1, -44,   8, -33, -30])

# vection multiplication
e = a * b
# print e --> array([ 812,  540,  660, 1134, 2911])

# vection division
f = a / b
# print f --> array([1.03571429, 0.18518519, 1.36363636, 0.38888889, 0.57746479])

# other operations can be performed respectively as above


import numpy as np
import random

# Part I

def get_order(n_samples):
    try:
        with open(str(n_samples) + '.txt') as fp:
            line = fp.readline()
            return list(map(int, line.split(',')))
    except FileNotFoundError:
        random.seed(1)
        indices = list(range(n_samples))
        random.shuffle(indices)
        return indices

def hinge_loss_single(feature_vector, label, theta, theta_0):
    """
    Finds the hinge loss on a single data point given specific classification parameters

    Args:
    :param feature_vector: A numpy array describing the given data point.
    :param label: A real valued number, the correct classification of the data point
    :param theta: A numpy array describing the linear classifier
    :param theta_0: A real valued number representing the offset parameter
    :return: A real number representing the hinge loss associated with the given datapoint and parameters
    """
    y = np.dot(theta, feature_vector) + theta_0
    loss = max(0.0, 1 - y * label)
    return loss

def hinge_loss_full(feature_matrix, labels, theta, theta_0):
    """
    Finds the total hinge loss on a set of data given specific classification parameters

    Args:
    :param feature_matrix: A numpy matrix describing the given data, Each row represents a single data point
    :param labels: A numpy array where Kth element of the array is the correct classification of the Kth row
                  of the feature matrix.
    :param theta: A numpy array describing the linear classification
    :param theta_0: A real valued number representing the offset parameter
    :return: A real number representing the hinge loss associated with the given dataset and parameters.
             This number should be the average hinge loss across all of the points in the feature matrix
    """
    loss = 0
    for idx in range(len(feature_matrix)):
        loss += hinge_loss_single(feature_matrix[idx], labels[idx], theta, theta_0)
    return loss / len(labels)

def perceptron_single_step_update(feature_vector, label, current_theta, current_theta_0):
    """
    Property updates the classification parameter, theta and theta_0 on a single step of the perceptron
    algorithm.

    Args:
    :param feature_vector: A numpy array describing a single data point.
    :param label: The correct classification of the feature vector
    :param current_theta: The current theta being used by the perceptron algorithm before this update
    :param current_theta_0: The current theta_0 being used by the perceptron algorithm before this update
    :return: A tuple where the first element is a numpy array with value of the theta after the current
             update has completed and the second element is a real valued number iwth the value of theta_0 after
             the current update has completed.
    """
    if label * (np.dot(current_theta, feature_vector) + current_theta_0) <= 1e-7:
        return current_theta + label * feature_vector, current_theta_0 + label
    return current_theta, current_theta_0

def perceptron(feature_matrix, labels, T):
    """
    Runs the full perceptron algorithm on a given set of data. Runs T
    iterations through the data set, there is no need to worry about
    stopping early.

    NOTE: Please use the previously implemented functions when applicable.
    Do not copy paste code from previous parts.

    NOTE: Iterate the data matrix by the orders returned by get_order(feature_matrix.shape[0])

    Args:
    :param feature_matrix: -  A numpy matrix describing the given data. Each row
            represents a single data point.
    :param labels: - A numpy array where the kth element of the array is the
            correct classification of the kth row of the feature matrix.
    :param T: - An integer indicating how many times the perceptron algorithm
            should iterate through the feature matrix.

    :return:  A tuple where the first element is a numpy array with the value of
    theta, the linear classification parameter, after T iterations through the
    feature matrix and the second element is a real number with the value of
    theta_0, the offset classification parameter, after T iterations through
    the feature matrix.
    """
    samples, features = feature_matrix.shape
    theta = np.zeros(features)
    theta_0 = 0.0
    for t in range(T):
        for i in get_order(samples):
            theta, theta_0 = perceptron_single_step_update(feature_matrix[i], labels[i], theta, theta_0)
    return theta, theta_0

def average_perceptron(feature_matrix, labels, T):
    """
    Runs the average perceptron algorithm on a given set of data. Runs T
    iterations through the data set, there is no need to worry about
    stopping early.

    NOTE: Please use the previously implemented functions when applicable.
    Do not copy paste code from previous parts.

    NOTE: Iterate the data matrix by the orders returned by get_order(feature_matrix.shape[0])

    Args:
    :param feature_matrix: -  A numpy matrix describing the given data. Each row
            represents a single data point.
    :param labels: - A numpy array where the kth element of the array is the
            correct classification of the kth row of the feature matrix.
    :param T: - An integer indicating how many times the perceptron algorithm
            should iterate through the feature matrix.

    :return: A tuple where the first element is a numpy array with the value of
    the average theta, the linear classification parameter, found after T
    iterations through the feature matrix and the second element is a real
    number with the value of the average theta_0, the offset classification
    parameter, found after T iterations through the feature matrix.

    Hint: It is difficult to keep a running average; however, it is simple to
    find a sum and divide.
    """
    (samples, features) = feature_matrix.shape
    theta = np.zeros(features)
    theta_sum = np.zeros(features)
    theta_0 = 0.0
    theta_0_sum = 0.0
    for t in range(T):
        for i in get_order(samples):
            theta, theta_0 = perceptron_single_step_update(feature_matrix[i], labels[i], theta, theta_0)
            theta_sum += theta
            theta_0_sum += theta_0
    return theta_sum / (samples * T), theta_0_sum / (samples * T)

def pegasos_single_step_update(feature_vector, label, l, eta, current_theta, current_theta_0):
    """
    Properly updates the classification parameter, theta and theta_0, on a
    single step of the Pegasos algorithm

    Args:
    :param feature_vector: - A numpy array describing a single data point.
    :param label:  - The correct classification of the feature vector.
    :param l: - The lamba value being used to update the parameters.
    :param eta:  - Learning rate to update parameters.
    :param current_theta: - The current theta being used by the Pegasos
            algorithm before this update.
    :param current_theta_0: - The current theta_0 being used by the
            Pegasos algorithm before this update.

    :return: A tuple where the first element is a numpy array with the value of
    theta after the current update has completed and the second element is a
    real valued number with the value of theta_0 after the current updated has
    completed.
    """
    multi = 1 - (eta * l)
    if label * (np.dot(feature_vector, current_theta) + current_theta_0) <= 1:
        return (multi * current_theta) + (eta * label * feature_vector), current_theta_0 + (eta * label)
    return multi * current_theta, current_theta_0

def pegasos(feature_matrix, labels, T, L):
    """
    Runs the Pegasos algorithm on a given set of data. Runs T
    iterations through the data set, there is no need to worry about
    stopping early.

    For each update, set learning rate = 1/sqrt(t),
    where t is a counter for the number of updates performed so far (between 1
    and nT inclusive).

    Args:
    :param feature_matrix: A numpy matrix describing the given data.
            Each row represents a single data point.
    :param labels: A numpy array where the kth element of the array is the correct classification of the kth row of the feature matrix.
    :param T:An integer indicating how many times the algorithm
            should iterate through the feature matrix.
    :param L:The lamba value being used to update the Pegasos
            algorithm parameters.
    :return: A tuple where the first element is a numpy array with the value of
    the theta, the linear classification parameter, found after T
    iterations through the feature matrix and the second element is a real
    number with the value of the theta_0, the offset classification
    parameter, found after T iterations through the feature matrix.
    """
    samples, features = feature_matrix.shape
    theta = np.zeros(features)
    theta_0 = 0
    count = 0
    for t in range(T):
        for i in get_order(samples):
            count += 1
            eta = 1.0 / np.sqrt(count)
            theta, theta_0 = pegasos_single_step_update(feature_matrix[i], labels[i], L, eta, theta, theta_0)
    return theta, theta_0

def classify(feature_matrix, theta, theta_0):
    """
    A classification function that uses theta and theta_0 to classify a set of
    data points.

    Args:
    :param feature_matrix:- A numpy matrix describing the given data. Each row
            represents a single data point.
                theta - A numpy array describing the linear classifier.
    :param theta: - A numpy array describing the linear classifier.
    :param theta_0: - A real valued number representing the offset parameter.
    :return:  A numpy array of 1s and -1s where the kth element of the array is
    the predicted classification of the kth row of the feature matrix using the
    given theta and theta_0. If a prediction is GREATER THAN zero, it should
    be considered a positive classification.
    """
    samples, features = feature_matrix.shape
    predictions = np.zeros(samples)
    for i in range(samples):
        feature_vector = feature_matrix[i]
        prediction = np.dot(theta, feature_vector) + theta_0
        if prediction > 0:
            predictions[i] = 1
        else:
            predictions[i] = -1
    return predictions

def accuracy(predictions, targets):
    """
    Given length-N vectors containing predicted and target labels,
    returns the percentage and number of correct predictions.
    """
    return (predictions == targets).mean()

def classifier_accuracy(
        classifier,
        train_feature_matrix,
        val_feature_matrix,
        train_labels,
        val_labels,
        **kwargs):
    """
    Trains a linear classifier and computes accuracy.
    The classifier is trained on the train data. The classifier's
    accuracy on the train and validation data is then returned.

    Args:
        classifier - A classifier function that takes arguments
            (feature matrix, labels, **kwargs) and returns (theta, theta_0)
        train_feature_matrix - A numpy matrix describing the training
            data. Each row represents a single data point.
        val_feature_matrix - A numpy matrix describing the validation
            data. Each row represents a single data point.
        train_labels - A numpy array where the kth element of the array
            is the correct classification of the kth row of the training
            feature matrix.
        val_labels - A numpy array where the kth element of the array
            is the correct classification of the kth row of the validation
            feature matrix.
        **kwargs - Additional named arguments to pass to the classifier
            (e.g. T or L)

    Returns: A tuple in which the first element is the (scalar) accuracy of the
    trained classifier on the training data and the second element is the
    accuracy of the trained classifier on the validation data.
    """
    theta, theta_0 = classifier(train_feature_matrix, train_labels, **kwargs)
    train_predictions = classify(train_feature_matrix, theta, theta_0)
    val_predictions = classify(val_feature_matrix, theta, theta_0)
    train_accuracy = accuracy(train_predictions, train_labels)
    validation_accuracy = accuracy(val_predictions, val_labels)
    return train_accuracy, validation_accuracy


# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: percent
#       format_version: '1.3'
#       jupytext_version: 1.4.2
#   kernelspec:
#     display_name: Python [conda env:mdd] *
#     language: python
#     name: conda-env-mdd-py
# ---

# %% jupyter={"outputs_hidden": false}
import numpy as np
import matplotlib.pyplot as plt
# %matplotlib inline

from scipy.fftpack import fft
# Number of samplepoints
N = 600
# sample spacing
T = 1.0 / 800.0
x = np.linspace(0.0, N*T, N)
y = np.sin(50.0 * 2.0*np.pi*x) + 0.5*np.sin(80.0 * 2.0*np.pi*x)
yf = fft(y)

halfN = np.int(N/2)

xf = np.linspace(0.0, 1.0/(2.0*T), halfN)
import matplotlib.pyplot as plt
plt.plot(xf, 2.0/N * np.abs(yf[0:halfN]))
plt.grid()
plt.show()

# %% jupyter={"outputs_hidden": false}
plt.figure()
plt.plot(x,y)
plt.plot(x,0.7*np.sin(50.0 * 2.0*np.pi*x) + 0.5*np.sin(80.0 * 2.0*np.pi*x),'r')

# %% jupyter={"outputs_hidden": false}
from scipy.signal.windows import hann
w = hann(N)
ywf = fft(y*w)
plt.figure()
plt.plot(xf[1:halfN], 2.0/N * np.abs(yf[1:halfN]), '-b')
plt.plot(xf[1:halfN], np.sqrt(8/3)*2.0/N * np.abs(ywf[1:halfN]), '-r')
plt.legend(['FFT', 'FFT w. window'])
plt.grid()
plt.show()


# Polynomial Regression

# Importing the libraries
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures

# Importing the dataset
dataset = pd.read_csv('Part 2 - Regression\Part 3 - Polynomial Linear Regression\Position_Salaries.csv')
X = dataset.iloc[:, 1:-1].values
y = dataset.iloc[:, -1].values

# Training the Linear Regression model on the whole dataset

lin_reg = LinearRegression()
lin_reg.fit(X, y)

# Training the Polynomial Regression model on the whole dataset
poly_feat = PolynomialFeatures(degree=4) #Higher the degree more likely the fit will be
x_poly = poly_feat.fit_transform(X)

lin_reg2 = LinearRegression()
lin_reg2.fit(x_poly,y)
# Visualising the Linear Regression results
plt.scatter(X,y, color='red')
plt.plot(X,lin_reg.predict(X), color='blue')
plt.title('Linear Regression')
plt.xlabel('position')
plt.ylabel('salary')
#plt.show()

# Visualising the Polynomial Regression results
plt.scatter(X,y, color='red')
plt.plot(X,lin_reg2.predict(x_poly), color='blue')
plt.title('polynomial Regression')
plt.xlabel('position')
plt.ylabel('salary')
#plt.show()
# Visualising the Polynomial Regression results (for higher resolution and smoother curve)
x_axis = np.arange(min(X),max(X),0.1)
x_axis = x_axis.reshape((len(x_axis),1))
plt.scatter(X,y, color='red')
plt.plot(x_axis,lin_reg2.predict(poly_feat.fit_transform(x_axis)), color='blue')
plt.title('polynomial Regression smooth')
plt.xlabel('position')
plt.ylabel('salary')
plt.show()
# Predicting a new result with Linear Regression
print(lin_reg.predict([[6.5]]))

# Predicting a new result with Polynomial Regression
print(lin_reg2.predict(poly_feat.fit_transform([[6.5]])))

Machine Learning concepts/Part 2 - Regression/Part 3 - Polynomial Linear Regression/polynomial_regression.py


"""
    Calculate the global reward penalized using the gini_coeficient coefficient of a numpy array of data.
    We use this value to provide a reward that account for the better distribution of MOS among the stations
    It substitutes the baseline that uses the average of MOS

    based on the statsdirect equation shown in
        http://www.statsdirect.com/help/default.htm#nonparametric_methods/gini_coeficient.htm

    Other references:
    * https://en.wikipedia.org/wiki/Gini_coefficient
    * https://towardsdatascience.com/gini_coeficient-coefficient-and-lorenz-curve-f19bb8f46d66
    * DORFMAN, Robert. A formula for the gini_coeficient coefficient. The review of economics and statistics, p. 146-149, 1979.

"""
import numpy as np

def scale_minmax(data):
    _min = np.amin(data)
    _max = np.amax(data)
    scaled = (data - _min)
    if _min != _max:
        scaled /= (_max - _min)
    return scaled

def gini_coeficient(user_data, epsilon=1e-18):
    """ Calculate the gini_coeficient coefficient of a numpy data.
        All values are treated equally,
        the values are first placed in ascending order, such that each x has rank i,

        @return: 0 if the data is homogeneous or 1 if the data
    """
    data = user_data.flatten()
    assert np.amin(data) >= 0, "Values cannot be negative"
    assert len(data.shape) == 1, "Data must be 1D"
    scaled = scale_minmax(data)

    # Sort data to calculate index
    data = np.sort(scaled)

    # Number of data elements:
    n = data.shape[0]
    # Index per data element:
    index = np.arange(1, n + 1)

    # gini_coeficient coefficient:
    coef_ = ((np.sum((2 * index - n - 1) * data)) / (n * np.sum(data) + epsilon))

    return coef_

def reward_gini(data):
    avg = np.average(data)
    g = gini_coeficient(data)
    r = (1 - g) * avg
    return r

if __name__ == "__main__":
    n = 20
    d = np.zeros(n)
    g = gini_coeficient(d)
    print("homogeneous zeros: {} ==> reward {}".format(g, reward_gini(d)))

    d[0] = 1
    g = gini_coeficient(d)
    print("heterogeneous - one 1: {} ==> reward {}".format(g, reward_gini(d)))

    d = np.full(n, 0.5)  # average MOS
    g = gini_coeficient(d)
    print("homogeneous 0.5: {} ==> reward {}".format(g, reward_gini(d)))

    d = np.ones(n)  # average MOS
    g = gini_coeficient(d)
    print("homogeneous 1: {} ==> reward {}".format(g, reward_gini(d)))


"""
    Authors: Artur Przybysz (s202384), Elena Mongelli(s181214), Ivan Knezevic (s202386)
"""
from typing import Dict

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from source.ex1_2 import plot_solution, interpolate

def u_function(x):
    return np.exp(-800 * (x - 0.4) ** 2) + 0.25 * np.exp(-40 * (x - 0.8) ** 2)

# TODO: plot second derivative versus mesh density

def point_on_line(p1, p2, p3):
    l2 = np.sum((p1 - p2) ** 2)
    t = max(0, min(1, np.sum((p3 - p1) * (p2 - p1)) / l2))
    projection = p1 + t * (p2 - p1)
    return projection

def compute_L2_error(length, slope1, slope2):
    """
    Computes integral of (a_1 * x - a_2 * x)^2 over [0, length]
    :param length:
    :param slope1:
    :param slope2:
    :return:
    """
    return length ** 3 * np.abs(slope2 - slope1) ** 2 * 1 / 3

def compute_error_decrease(fun, VX, EToV) -> Dict[int, float]:
    """
    Computes estimate of possible error decrease for each element in mesh.

    :param fun: Function float -> float
    :param VX: dict from point id to its position on x axis.
    :param EToV: dict from element id to a tuple of its boundary points.
    """
    L2_loss = dict()
    for e, (idx1, idx2) in EToV.items():
        x1 = VX[idx1]
        x2 = VX[idx2]
        y1 = fun(x1)  # This line should be updated in 1.7
        y2 = fun(x2)  # This line should be updated in 1.7
        x_half = (x1 + x2) / 2
        y_half = fun(x_half)

        slope0 = (y2 - y1) / (x2 - x1)
        slope1 = (y_half - y1) / (x_half - x1)
        slope2 = (y2 - y_half) / (x2 - x_half)

        L2_loss1 = compute_L2_error(x_half - x1, slope0, slope1)
        L2_loss2 = compute_L2_error(x_half - x1, slope0, slope2)
        L2_loss[e] = np.sqrt(L2_loss1 + L2_loss2)

    return L2_loss

def refine_marked(EtoV, VX, idxMarked):
    # [EToVfine, VXfine]
    max_e_idx = max(EtoV.keys())
    max_v_idx = max(VX.keys())
    for idx in idxMarked:
        x1_idx, x2_idx = EtoV[idx]
        x1 = VX[x1_idx]
        x2 = VX[x2_idx]
        x_mid = (x1 + x2) / 2

        if x_mid in VX.values():
            continue

        EtoV[idx] = (x1_idx, max_v_idx + 1)
        EtoV[max_e_idx + 1] = (max_v_idx + 1, x2_idx)
        VX[max_v_idx + 1] = x_mid

        max_e_idx += 1
        max_v_idx += 1
    return EtoV, VX

def refine_until_converged(VX, EtoV, fun, tolerance):
    L2_loss = compute_error_decrease(fun, VX, EtoV)
    plot_errors(L2_loss, tolerance, title="Errors before convergence")

    step = 0
    while max(L2_loss.values()) >= tolerance:
        marked_idx = [k for k, v in sorted(L2_loss.items(), key=lambda t: t[1])[-1:]]
        EToV, VX = refine_marked(EtoV, VX, marked_idx)
        L2_loss = compute_error_decrease(u_function, VX, EtoV)
        step += 1
    plot_errors(L2_loss, tolerance, title="Errors after convergence")
    return VX, EtoV

def plot_errors(L2_loss, tol, title):
    # TODO: log scale for y!
    plt.axhline(y=tol, color='r', linestyle='-')
    plt.title(title)
    plt.xlabel("Id of element")
    plt.ylabel("L2 error value")
    y = [v for v in L2_loss.values()]
    x = [v for v in L2_loss.keys()]
    if len(x) > 10:
        plt.xticks(x[::5])
    else:
        plt.xticks(x)

    plt.scatter(x, y)
    plt.yscale("log")
    plt.show()

def main():
    """
    QUESTION: "How can the metric be computed? Detail this."
    ANSWER: Having three points: A, B and C, which are left, right bounds and the true point in the middle, we can
    estimate the L2 error using an analytical method. Using basic geometry I find the projection of C onto line
    connecting A and B. This allows to find two perpendicular triangles, which are later squared and the area under
    parabola in the range [0, triangle_base] is computed.
    """
    """
    NOTE1: Even if tolerance is set very low, the function could omit some areas! For example if initialized with
        3 points, then the left local (and global at the same time) maximum will be omitted. Only when more points
        are used for initialization, this area is 'explored' more than the right local maximum.
        Show it on examples.
    """

    mesh = np.linspace(start=0, stop=1, num=3)
    u_hat = u_function(mesh)

    x = np.linspace(start=0, stop=1, num=500)
    u_x = u_function(x)
    u_hat = interpolate(u_hat, mesh, x)
    plot_solution(u_hat, u_x, x, mesh)

    VX = {i: x_i for i, x_i in enumerate(mesh)}
    EtoV = {n: (n, n + 1) for n in range(len(mesh) - 1)}

    VX, EtoV = refine_until_converged(VX, EtoV, u_function, tolerance=0.0001)
    mesh2 = np.array(list(sorted([v for v in VX.values()])))
    u_hat = interpolate(u_function(mesh2), mesh2, x)
    plot_solution(u_hat, u_x, x, mesh2)
    plt.hist(mesh2, bins=21, density=True, label="mesh bounds histogram")
    plt.plot(x, u_x, label="$u(x)$")
    plt.legend()
    plt.show()

if __name__ == '__main__':
    main()


import numpy as np
n,f0,fn = 32,1.,3.
g = [np.sin(x) for x in np.linspace(0, 2*np.pi, n)]
A = np.matrix(np.zeros((n-1,n-2)))
np.fill_diagonal(A,      1)
np.fill_diagonal(A[1:], -1)
b = np.matrix([[g[i]-g[i-1]] for i in range(1,n)])
b[ 0,0] = b[ 0,0] + f0
b[-1,0] = b[-1,0] - fn
f = [f0] + (np.linalg.inv(A.T*A)*A.T*b).T.tolist()[0] + [fn]

import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "serif"
plt.rcParams["mathtext.fontset"] = "dejavuserif"
plt.rcParams['text.usetex'] = True
plt.rc('font', size=20)
fig,ax = plt.subplots(1, figsize=(6.40,6.40),dpi=150)
for item in ([ax.title, ax.xaxis.label, ax.yaxis.label] + ax.get_xticklabels() + ax.get_yticklabels()):
    item.set_fontsize(20)
x = np.linspace(0, 2*np.pi, n)
plt.plot(x, f, linewidth=3, label='$f(x)$')
plt.plot(x, g, linewidth=3, label='$g(x) = \sin x$')
plt.scatter([x[0], x[-1]], [f0,fn], color='red', edgecolors='black',s=200)
plt.legend(frameon=False)
plt.tight_layout()
plt.gca().set_aspect('equal', adjustable='box')
#plt.savefig('prepie.png', bbox_inches='tight')
plt.show()


import numpy as np
from typing import List

from pynet.functional.abstract import Function
from pynet.tensor import Tensor

class Sigmoid(Function):
    """Function represents element-wise sigmoid function.

    Sigmoid function is defined as S(x) = 1 / (1 + e^(-x)). Function takes one
    tensor as input (i. e. len(x) == 1 in the forward method) and outputs an
    tensor representing the sigmoid function result.
    """

    def __init__(self) -> None:
        """Ctor"""
        super().__init__()

    def forward(self, x: List[Tensor]) -> Tensor:
        assert len(x) == 1, "Sigmoid function takes 1 parameter"

        sig = 1.0 / (1.0 + np.exp(-x[0].ndarray))
        self._stored_results["sig"] = sig

        return Tensor(sig)

    def backward(self, y: Tensor) -> List[Tensor]:
        sig = self._stored_results["sig"]

        dsig = np.multiply(sig, 1.0 - sig)
        dsig = np.multiply(dsig, y.ndarray)

        return [Tensor(dsig)]


from numpy import trace, ones, zeros, diag, eye, diagflat
from numpy.linalg import eig, cholesky
from numpy.random import multivariate_normal as mvnrnd

from TwistFPMomMatch import TwistFPMomMatch

def SimQuadNFP(a, b, c, mu, sigma2, j_):
    # This function generates QuadN simulations with Flexible Probabilities
    # whose first moment match the theoretical moment
    #  INPUTS
    #   a      : [scalar] parameter
    #   b      : [vector] (n_ x 1) parameter
    #   c      : [matrix] (n_ x n_) symmetric matrix parameter
    #   mu     : [vector] (n_ x 1) normal expectations
    #   sigma2 : [matrix] (n_ x n_) normal covariances
    #   j_     : [scalar] number of simulations
    #  OPS
    #   Y      : [matrix] (n_ x j_) MC scenarios
    #   p_     : [vector] (1 x j_) twisted flexible probabilities

    # For details on the exercise, see here .

    ## Code

    n_ = mu.shape[0] # number of variables

    # Step 1. Cholesky
    l = cholesky(sigma2)

    # Step 2. Eigen-decomposition
    Diag_lamda, _ = eig(l.T@c@l)

    # Step 3. Change of variables
    gamma = l.T@(b+2*c@mu)

    # Step 4. MC scenarios with Flexible Probabilities
    Z = mvnrnd(zeros(n_), eye((n_)), j_)
    Z = Z.T
    p = ones((1,j_))/j_
    Y = a + b.T@mu + mu.T@c@mu + gamma.T@Z + Diag_lamda.reshape(1,-1)@(Z**2)

    # Step 5. Moment-matching
    mu_ = a + b.T@mu + mu.T@c@mu + trace(c@sigma2)
    p_ = TwistFPMomMatch(Y, p, mu_)
    return Y, p_


''' utils for ndcg calculation '''

import torch
import numpy as np

DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'

def np_dcg(rel_list):
    if type(rel_list) is not np.ndarray:
        rel_list = np.array(rel_list)

    return np.sum(
        (2 ** rel_list - 1) / np.log2(np.arange(2, rel_list.size + 2))
    )

def np_idcg(rel_list):
    rel_list = np.sort(rel_list)[::-1]
    return np_dcg(rel_list)

# torch ndcg util functions
def dcg(rel_tensor, k=None):
    if not torch.is_tensor(rel_tensor):
        rel_tensor = torch.tensor(rel_tensor).to(DEVICE)

    rank_tensor = torch.arange(
        1, rel_tensor.size(-1) + 1, dtype=torch.float).to(DEVICE)

    if k is None:
        k = rel_tensor.size(-1)

    return (
        (2 ** rel_tensor - 1) / (rank_tensor + 1).log2()
    )[..., :k].sum(-1)

def idcg(rel_tensor, k=None):
    rel_tensor, _ = rel_tensor.sort(-1, descending=True)
    return dcg(rel_tensor)

def ndcg(rel_list, k=None, method='torch'):
    '''
    Input:
      rel_list: list of relevance, (*, N) tensor
      method: use numpy or torch to calculate
    Output:
      ndcg: (*) tensor
    '''

    if method == 'numpy':
        return np_dcg(rel_list) / np_idcg(rel_list)

    return dcg(rel_list, k) / idcg(rel_list, k)


import numpy as np
import math
class Activation:
    #stil need to improve reLU and numpy datatype as they always overflow when use RELU
    def __init__(self): return None

    def ReLU(self, x, Derivative=False):
        x = np.clip(x, -600, 600)
        if not Derivative:
            return np.maximum(0,x)
        else: return np.greater(x, 0) + 0
    def LeakyReLU(self, x, Derivative=False):
        x = np.clip(x, -600, 600)
        if not Derivative:
            return np.maximum(2.0/10.0*x, x)
        else: return (np.greater(x, 0)+0) + (np.less(x,0.001)+0)*2.0/10.0
    def Sigmoid(self, x, Derivative=False):
        x = np.clip(x, -600, 600)
        if not Derivative:
            return 1/(1+np.exp(-x))
        else: return x * (1 - x)
    def Tanh(self, x, Derivative=False):
        x = np.clip(x, -600, 600)
        if not Derivative:
            return np.tanh(x)
        else: return 1 - x**2
    def Softmax(self, x, Derivative=False):
        x = np.clip(x, -600, 600)
        if not Derivative:
            exp_sum = np.sum( np.exp(x) )
            return np.exp(x)/exp_sum
        else: return x * (1 - x)
    def Linear(self, x, Derivative=False):
        x = np.clip(x, -600, 600)
        if not Derivative: return x
        else: return 1


"""
Author: Peratham Wiriyathammabhum

"""
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# import scipy.sparse.linalg as linalg
from numpy import linalg as LA
from numpy import inf

from graph_utils import geodesic_graph

def isomap(X, mode='affinity', knn=3, eta=0.01):
	"""
	Perform isomap on an input row matrix X.
	See: Tenenbaum, Joshua B., Vin De Silva, and John C. Langford.
	"A global geometric framework for nonlinear dimensionality reduction."
	science 290.5500 (2000): 2319-2323. http://www.robots.ox.ac.uk/~az/lectures/ml/tenenbaum-isomap-Science2000.pdf
	"""
	ni, nd = X.shape

	G = geodesic_graph(X, mode=mode, knn=knn, eta=eta)
	# The operator t is defined by t(D) = -HSH/2, where S
	# is the matrix of squared distances {Sij = Dij**2}, and H is
	# the “centering matrix” {Hij = dij - 1/N}
	G[G == inf] = 0
	G[np.isnan(G)] = 0

	G = G ** 2
	G = G - sum(G, 1).T / ni
	G = G - sum(G, 1) / ni
	G = G + sum(G[:]) / (ni ** 2)
	G = -0.5 * G

	G[G == inf] = 0
	G[np.isnan(G)] = 0

	w, v = LA.eig(G)
	ind = np.argsort(-w, axis=0) # sorting descending
	w = w[ind]
	v = v[:, ind]
	sw = np.sqrt(w)

	v = v * sw

	X_r = v[:,0:2] # projection matrix
	return X_r

def main(opts):
	knn = opts['knn']
	eta = opts['eta']
	mode = opts['mode']

	# load data
	from sklearn import datasets
	X, color = datasets.make_swiss_roll(n_samples=2500)

	# dimred
	X_r = isomap(X, mode=mode, knn=knn, eta=eta)

	# plot
	fig = plt.figure()

	ax = fig.add_subplot(211, projection='3d')
	ax.scatter(X[:, 0], X[:, 1], X[:, 2], c=color, cmap=plt.cm.Spectral)

	ax.set_title("Original data")
	ax = fig.add_subplot(212)
	ax.scatter(X_r[:, 0], X_r[:, 1], c=color, cmap=plt.cm.Spectral)
	plt.axis('tight')
	plt.xticks([]), plt.yticks([])
	plt.title('Projected data')
	plt.show()
	input("Press Enter to continue...")

	return

if __name__ == '__main__':
	import argparse

	parser = argparse.ArgumentParser(description='run isomap.')
	parser.add_argument('--knn', dest='knn',
					  help='number of neighbors',
					  default=4, type=int)
	parser.add_argument('--eta', dest='eta',
					  help='distance eta',
					  default=0.01, type=int)
	parser.add_argument('--mode', dest='mode',
					  help='graph construction mode \in {affinity, nearestneighbor}',
					  default='affinity', type=str)
	args = parser.parse_args()
	opts = vars(args)

	main(opts)