from numpy import genfromtxt
import pandas as pd
import numpy as np
from pandas.core import base

"""
df = pd.read_csv('baseball_small.csv', sep=',')
X1 = df[['RS','RA','W','OBP','SLG','BA']]
y = df['OOBP']
X=np.asmatrix(X1)
"""

baseball_data = genfromtxt('baseball_small.csv',delimiter=',')
print(baseball_data[1:48,3:9])
X = baseball_data[1:48,3:9]
y = baseball_data[1:48,-2]
X_test=baseball_data[49,3:9]
print(X)
print(y)

XT = np.matrix.transpose(X)
XT_X = np.matmul(XT, X)
XT_y = np.matmul(XT, y)
betas = np.matmul(np.linalg.inv(XT_X), XT_y)
print(betas)
print(X_test)
print(betas.dot(np.transpose(X_test)))
print(f'Value in csv file={baseball_data[49,-2]}')


"""
Demonstration script for quadratic interpolation.
Modified for the problem described in Homework 2 problem #1.
"""

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

# Set up linear system to interpolate through data points:

# Data points:
xi = np.array([-1., 1., 2])
yi = np.array([0., 4., 3.])

# Second row of A changed from demo1.py:
A = np.array([[1., -1., 1.], [1., 1., 1.], [1., 2., 4.]])
b = yi

# Solve the system:
c = solve(A,b)

print "The polynomial coefficients are:"
print c

# Plot the resulting polynomial:
x = np.linspace(-2,3,1001)   # points to evaluate polynomial
y = c[0] + c[1]*x + c[2]*x**2

plt.figure(1)       # open plot figure window
plt.clf()           # clear figure
plt.plot(x,y,'b-')  # connect points with a blue line

# Add data points  (polynomial should go through these points!)
plt.plot(xi,yi,'ro')   # plot as red circles
plt.ylim(-2,8)         # set limits in y for plot

plt.title("Data points and interpolating polynomial")

plt.savefig('hw2a.png')   # save figure as .png file


import cvxpy as cp
from cvxpy.expressions.cvxtypes import power
import numpy as np
from math import sqrt
import numpy as np
from scipy.linalg import fractional_matrix_power

def sdp(rho0, rho1):
    n = len(rho0)

    # A and B are positive semidefinite matrices. B is an invertible matrix.
    A = rho0
    B = rho0 + rho1
    # Define and solve the CVXPY problem.
    # Create a symmetric matrix variable.
    P = cp.Variable((n, n), symmetric=True)
    x = fractional_matrix_power(B, -0.5)
    y = fractional_matrix_power(B, 0.5)
    gamma = y @ P @ y
    # The operator << denotes matrix inequality.
    # constraints = [cp.trace(B @ P) <= 1]
    # constraints += [P >> 0]
    # constraints += [P << np.eye(2)]
    constraints = [gamma << B]
    constraints += [P >> 0]
    constraints += [cp.trace(gamma) <= 1]
    prob = cp.Problem(cp.Maximize(cp.trace(x @ A @ x @ gamma)),
                      constraints)
    prob.solve()

    # Print result.
    # print("Input rho_0 is:")
    # print(rho0)
    # print("Input rho_1 is:")
    # print(rho1)
    '''
    Change desired decimal places for solution here
    '''
    value = round(prob.value, ndigits=5)
    # solution = np.around(P.value, decimals=3)
    # gammaResult = np.around(y @ P.value @ y, decimals=3)
    # print("The optimal value is", value)
    # print("A solution P is")
    # print(solution)
    # print("Gamma result is")
    # print(gammaResult)

    return value

if __name__ == "__main__":
    '''
    Change rho_0 and rho_1 here
    '''
    rho0 = np.array([[0.25, sqrt(3)/4], [sqrt(3)/4, 0.75]])
    # rho0 = np.array([[0.5625, 3 * sqrt(7)/16], [3 * sqrt(7)/16, 7/16]])    rho1 = np.array([[0.5, 0], [0, 0.5]])
    # rho0 = np.array([[1, 0], [0, 0]])
    rho1 = np.array([[0.5, 0], [0, 0.5]])
    # rho0 = np.array([[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])
    # rho1 = np.array([[0.25, 0, 0, 0], [0, 0.25, 0, 0],
    #                  [0, 0, 0.25, 0], [0, 0, 0, 0.25]])
    sdp(rho0, rho1)

import numpy,from scipy,import cvxpy,from cvxpy


"""
Triangular norms (t-norms): commutative, associative, non-decreasing binary operators on `[0, 1]` with identity 1.
T-norms generalise classical conjunction for fuzzy logic.

Each function aggregates the values of the provided array along the specified axis.
"""

import numpy as np

__all__ = [
    'goguen_t_norm', 'heyting_t_norm', 'lukasiewicz_t_norm',
]

def goguen_t_norm(a, axis):
    """
    `x * y`; also known as *product* t-norm.

    Parameters
    ----------
    a: np.array
        Input array of values. All values should lie in `[0, 1]`.

    axis : int, default=-1
        The axis along which values are aggregated.

    Returns
    -------
    b: np.array
        Aggregated values, array with the same shape as `a`, except for the specified axis.
    """
    return np.prod(a, axis=axis)

def heyting_t_norm(a, axis):
    """
    `min(x, y)`; also known as *Gödel* or *minimum* t-norm.

    Parameters
    ----------
    a: np.array
        Input array of values. All values should lie in `[0, 1]`.

    axis : int, default=-1
        The axis along which values are aggregated.

    Returns
    -------
    b: np.array
        Aggregated values, array with the same shape as `a`, except for the specified axis.
    """
    return np.min(a, axis=axis)

def lukasiewicz_t_norm(a, axis):
    """
    `max(x + y - 1, 0)`

    Parameters
    ----------
    a: np.array
        Input array of values. All values should lie in `[0, 1]`.

    axis : int, default=-1
        The axis along which values are aggregated.

    Returns
    -------
    b: np.array
        Aggregated values, array with the same shape as `a`, except for the specified axis.
    """
    return np.maximum(np.sum(a, axis=axis) - (a.shape[axis] - 1), 0)


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

from __future__ import division, print_function

import numpy as np
from scipy.signal import remez, freqz
import matplotlib.pyplot as plt

def bellanger_estimate(deltap, deltas, width, fs):
    """
    Estimate the number of taps required for the given filter specifications.
    """
    n = (-2/3)*np.log10(10*deltap*deltas)*fs/width
    n = int(np.ceil(n))
    return n

def remez_lowpass(deltap, deltas, cutoff, width, fs):
    """ estimate coeffient of each taps"""
    numtaps = bellanger_estimate(deltap, deltas, width, fs)
    numtaps |= 1  # Bitwise OR with 1 to ensure an odd number of taps.
    trans_lo = cutoff - 0.5*width
    trans_hi = cutoff + 0.5*width
    taps = remez(numtaps,
                 bands=[0, trans_lo, trans_hi, 0.5*fs],
                 desired=[1, 0],
                 weight=[1/deltap, 1/deltas],
                 fs=fs)
    return taps

#---------------------------------------
# User inputs...

# Frequency values in Hz
fs = 1000.0
cutoff = 180.0
width = 30.0
# Desired pass band ripple and stop band attenuation
deltap = 0.005
deltas = 0.002

print(u"Pass band:  1 ± %g ([%.3g, %.3g] dB)" %
      (deltap, 20*np.log10(1 - deltap), 20*np.log10(1 + deltap)))
print("Stop band rejection: %g (%.3g dB)" % (deltas, -20*np.log10(deltas),))

#---------------------------------------
# Design the filter...

taps = remez_lowpass(deltap, deltas, cutoff, width, fs)

#----------------------------------------
# Plot the frequency response...

upper_ripple_db = 20*np.log10(1 + deltap)
lower_ripple_db = 20*np.log10(1 - deltap)
stop_db = -20*np.log10(deltas)

print("Inputs")
print("------")
print("fs:", fs)
print("cutoff:", cutoff)
print("transition band width:", width)
print("deltap:", deltap, " (%.3f dB)" % (-20*np.log10(deltap),))
print("deltas:", deltas, " (%.3f dB)" % (-20*np.log10(deltas),))
print()
print("Design")
print("------")
print("numtaps:", len(taps))

w, h = freqz(taps, worN=8000)
w *= 0.5*fs/np.pi

cutoff_lower_trans = cutoff - 0.5*width
cutoff_upper_trans = cutoff + 0.5*width

plt.figure(figsize=(4.0, 4.6))
plt.subplot(3, 1, 1)

plt.plot(w, 20*np.log10(np.abs(h)))

plt.plot([0, cutoff_lower_trans], [upper_ripple_db, upper_ripple_db], 'r',
         alpha=0.4)
plt.plot([0, cutoff_lower_trans], [lower_ripple_db, lower_ripple_db], 'r',
         alpha=0.4)

plt.plot([cutoff_upper_trans, 0.5*fs], [-stop_db, -stop_db], 'r', alpha=0.4)

plt.axvline(cutoff_lower_trans, color='k', alpha=0.4, linewidth=1)
plt.axvline(cutoff_upper_trans, color='k', alpha=0.4, linewidth=1)

widthstr = '%g Hz' % width
if cutoff < 0.25*fs:
    lefttext = ''
    righttext = widthstr
else:
    lefttext = widthstr
    righttext = ''
plt.annotate(righttext, (cutoff_upper_trans, -0.5*stop_db),
             xytext=(18, 0),
             textcoords='offset points',
             va='center', ha='left',
             arrowprops=dict(arrowstyle='->'))
plt.annotate(lefttext, (cutoff_lower_trans, -0.5*stop_db),
             xytext=(-18, 0),
             textcoords='offset points',
             va='center', ha='right',
             arrowprops=dict(arrowstyle='->'))

plt.ylim(-1.25*stop_db, 10)
plt.grid(alpha=0.25)
plt.ylabel('Gain (dB)')
plt.title("Lowpass Filter\nOptimal Remez Design",
          fontsize=10)

plt.subplot(3, 1, 2)

plt.plot(w, np.abs(h))

plt.plot([0, cutoff_lower_trans], [1 + deltap, 1 + deltap], 'r', alpha=0.4)
plt.plot([0, cutoff_lower_trans], [1 - deltap, 1 - deltap], 'r', alpha=0.4)

plt.plot([cutoff_upper_trans, 0.5*fs], [deltas, deltas], 'r', alpha=0.4)

plt.axvline(cutoff_lower_trans, color='k', alpha=0.4, linewidth=1)
plt.axvline(cutoff_upper_trans, color='k', alpha=0.4, linewidth=1)

plt.xlim(0, (cutoff + 0.6*width))
plt.ylim(1 - 1.6*deltap, 1 + 1.6*deltap)

plt.ylabel('Gain')

plt.grid(alpha=0.25)

plt.subplot(3, 1, 3)
desired = w < cutoff
deviation = np.abs(np.abs(h) - desired)
deviation[(w >= cutoff-0.5*width) & (w <= cutoff + 0.5*width)] = np.nan
plt.plot(w, deviation)
plt.plot([0, cutoff - 0.5*width], [deltap, deltap], 'r',
         linewidth=1, alpha=0.4)
plt.plot([cutoff + 0.5*width, 0.5*fs], [deltas, deltas], 'r',
         linewidth=1, alpha=0.4)
plt.ylabel(u'|A(ω) - D(ω)|')
plt.grid(alpha=0.25)

plt.xlabel('Frequency (Hz)')

plt.tight_layout()
plt.savefig("opt_lowpass.pdf")


import matplotlib.pyplot as plt # matplot lib is the premiere plotting lib for Python: https://matplotlib.org/
import numpy as np # numpy is the premiere signal handling library for Python: http://www.numpy.org/
import scipy as sp # for signal processing
from scipy import signal
from scipy.spatial import distance
import librosa
import random

### SINE AND COSINE GENERATOR FUNCTIONS ###

def create_sine_waves(freqs, sampling_rate, total_time_in_secs = None, return_time = False):
    '''Creates multiple sine waves corresponding to the freq array, sampling rate, and length

       Returns a tuple list of (freq, sine_wave) or (freq, (time, sine_wave))
       depending on whether return_time is True or False
    '''
    sine_waves = []
    for freq in freqs:
        sine_waves.append((freq, create_sine_wave(freq, sampling_rate, total_time_in_secs, return_time)))
    return sine_waves

def create_sine_wave_sequence(freqs, sampling_rate, time_per_freq = None, starting_amplitudes = None,
                             ending_amplitudes = None):

    '''
    Creates a sine wave sequence at the given frequencies and sampling rate. You can control
    the time per frequency via time_per_freq and the starting and ending amplitudes of each signal

    Parameters:
        freqs (array): array of frequencies
        sampling_rate (num): sampling rate
        time_per_freq (float or list): If a float, creates all sine waves of that length (in secs).
            If an array, takes the time per frequency (in secs). If None, sets all sine waves to length 1 sec.
        starting_amplitudes (array): List of starting amplitudes for each freq (one by default)
        ending_amplitudes (array): list of ending amplitudes for each freq (zero by default)
    '''
    if starting_amplitudes is None:
        starting_amplitudes = np.ones(len(freqs))

    if ending_amplitudes is None:
        ending_amplitudes = np.zeros(len(freqs))

    if time_per_freq is None:
        time_per_freq = np.ones(len(freqs))
    elif isinstance(time_per_freq, (list, tuple, np.ndarray)) is False:
        time_per_freq = np.full(len(freqs), time_per_freq)

    signal_sequence = np.array([])
    for i, freq in enumerate(freqs):
        signal = create_sine_wave(freq, sampling_rate, time_per_freq[i])

        # Currently linear interpolation between start and end amplitudes
        # But we could expand this later to exponential, etc.
        amplitudes = np.linspace(starting_amplitudes[i], ending_amplitudes[i], num=len(signal))
        signal_with_amplitudes = signal * amplitudes
        signal_sequence = np.concatenate((signal_sequence, signal_with_amplitudes))

    return signal_sequence

def create_composite_sine_wave(freqs, sampling_rate, total_time_in_secs, amplitudes = None,
                              use_random_amplitudes = False, return_time = False):
    '''Creates a composite sine wave with the given frequencies and amplitudes'''

    if amplitudes is None and use_random_amplitudes is False:
        amplitudes = np.ones(len(freqs))
    elif amplitudes is None and use_random_amplitudes is True:
        amplitudes = np.random.uniform(low = 0.1, high = 1, size=(len(freqs)))

    time = np.arange(total_time_in_secs * sampling_rate) / sampling_rate
    signal_composite = np.zeros(len(time)) # start with empty array
    for i, freq in enumerate(freqs):
        # set random amplitude for each freq (you can change this, of course)
        signal = amplitudes[i] * create_sine_wave(freq, sampling_rate, total_time_in_secs)
        signal_composite += signal

    if return_time is False:
        return signal_composite
    else:
        return (time, signal_composite)

def create_sine_wave(freq, sampling_rate, total_time_in_secs = None, return_time = False):
    '''Creates a sine wave with the given frequency, sampling rate, and length'''

    # if the total time in secs is None, then return one period of the wave
    if total_time_in_secs is None:
        total_time_in_secs = 1 / freq

    # Create an array from 0 to total_time_in_secs * sampling_rate (and then divide by sampling
    # rate to get each time_step)
    time = np.arange(total_time_in_secs * sampling_rate) / sampling_rate

    # Could also generate this signal by:
    # time = np.linspace(0, total_time_in_secs, int(total_time_in_secs * sampling_rate), endpoint=False)

    sine_wave = np.sin(2 * np.pi * freq * time)

    # or, once the sample is made:
    # time = np.linspace(0, len(s) / sampling_rate, num=len(s))

    if return_time is False:
        return sine_wave
    else:
        return (time, sine_wave)

def create_cos_wave(freq, sampling_rate, total_time_in_secs = None, return_time = False):
    '''Creates a cos wave with the given frequency, sampling rate, and length'''

     # if the total time in secs is None, then return one period of the wave
    if total_time_in_secs is None:
        total_time_in_secs = 1 / freq

    # Create an array from 0 to total_time_in_secs * sampling_rate (and then divide by sampling
    # rate to get each time_step)
    time = np.arange(total_time_in_secs * sampling_rate) / sampling_rate
    cos_wave = np.cos(2 * np.pi * freq * time)

    if return_time is False:
        return cos_wave
    else:
        return (time, cos_wave)

def get_random_xzoom(signal_length, fraction_of_length):
    '''Returns a tuple of (start, end) for a random xzoom amount'''
    zoom_length = int(signal_length * fraction_of_length)
    random_start = random.randint(0, signal_length - zoom_length)
    xlim_zoom = (random_start, random_start + zoom_length)
    return xlim_zoom

def map(val, start1, stop1, start2, stop2):
    '''Similar to Processing and Arduino's map function'''
    return ((val-start1)/(stop1-start1)) * (stop2 - start2) + start2

def remap(val, start1, stop1, start2, stop2):
    '''Similar to Processing and Arduino's map function'''
    return ((val-start1)/(stop1-start1)) * (stop2 - start2) + start2

### SIGNAL MANIPULATION FUNCTIONS ###

# While numpy provides a roll function, it does not appear to provide a shift
# https://stackoverflow.com/q/30399534
# So, lots of people have implemented their own, including some nice benchmarks here:
# https://stackoverflow.com/a/42642326
def shift_array(arr, shift_amount, fill_value = np.nan):
    '''Shifts the array either left or right by the shift_amount (which can be negative or positive)

       From: https://stackoverflow.com/a/42642326
    '''
    result = np.empty_like(arr)
    if shift_amount > 0:
        result[:shift_amount] = fill_value
        result[shift_amount:] = arr[:-shift_amount]
    elif shift_amount < 0:
        result[shift_amount:] = fill_value
        result[:shift_amount] = arr[-shift_amount:]
    else:
        result[:] = arr
    return result

### SIGNAL ANALYSIS FUNCTIONS ###

# TODO: update get_top_n_frequency_indices_sorted so that you can specify a min_gap
#       between top freqs (so if two top freqs are close together, one can be skipped)
def get_top_n_frequency_indices_sorted(n, freqs, amplitudes):
    '''Gets the top N frequency indices (sorted)'''
    ind = np.argpartition(amplitudes, -n)[-n:] # from https://stackoverflow.com/a/23734295
    ind_sorted_by_coef = ind[np.argsort(-amplitudes[ind])] # reverse sort indices

    return ind_sorted_by_coef

def calc_zero_crossings(s, min_gap = None):
    '''Returns the number of zero crossings in the signal s

    This method is based on https://stackoverflow.com/q/3843017

    Parameters:
    s: the signal
    min_gap: the minimum gap (in samples) between zero crossings
    TODO:
    - could have a mininum height after the zero crossing (within some window) to eliminate noise
    '''
    # I could not get the speedier Pythonista solutions to work reliably so here's a
    # custom non-Pythony solution
    cur_pt = s[0]
    zero_crossings = []
    last_zero_crossing_idx = None
    last_zero_cross_idx_saved = None
    for i in range(1, len(s)):
        next_pt = s[i]
        zero_crossing_idx = None

        # There are three cases to check for:
        #  1. If the cur_pt is gt zero and the next_pt is lt zero, obviously a zero crossing.
        #     Similarly, if the next_pt is gt zero and the cut_pt is lt zero, again a zero crossing
        #  2. If the cur_pt is zero and the next_pt gt zero, then we walk back to see when zero
        #     was first "entered"
        #  3. Finally, if the cut_pt is zero and the next_pt lt zero, we again walk back to see
        #     when zero was first "entered"
        if ((next_pt < 0 and cur_pt > 0) or (next_pt > 0 and cur_pt < 0)):
            # if we're here, a zero crossing occurred
            zero_crossing_idx = i

        elif cur_pt == 0 and next_pt > 0:
            # check for previous points less than 0
            # as soon as tmp_pt is not zero, we are done
            tmp_pt = cur_pt
            walk_back_idx = i
            while(tmp_pt == 0 and walk_back_idx > 0):
                walk_back_idx -= 1
                tmp_pt = s[walk_back_idx]

            if tmp_pt < 0:
                zero_crossing_idx = i

        elif cur_pt == 0 and next_pt < 0:
            # check for previous points greater than 0
            # as soon as tmp_pt is not zero, we are done
            tmp_pt = cur_pt
            walk_back_idx = i
            while(tmp_pt == 0 and walk_back_idx > 0):
                walk_back_idx -= 1
                tmp_pt = s[walk_back_idx]

            if tmp_pt > 0:
                zero_crossing_idx = i

        # now potentially add zero_crossing_idx to our list
        if zero_crossing_idx is not None:
            # potentially have a new zero crossing, check for other conditions
            if last_zero_cross_idx_saved is None or \
               last_zero_cross_idx_saved is not None and min_gap is None or \
               (min_gap is not None and (i - last_zero_cross_idx_saved) > min_gap):

                zero_crossings.append(zero_crossing_idx) # save the zero crossing point
                last_zero_cross_idx_saved = zero_crossing_idx

            last_zero_crossing_idx = zero_crossing_idx

        cur_pt = s[i]
    return zero_crossings

##### VISUALIZATION CODE ######
def plot_signal_to_axes(ax, s, sampling_rate, title=None, signal_label=None, marker=None):
    '''Plots a sine wave s with the given sampling rate

    Parameters:
    ax: matplot axis to do the plotting
    s: numpy array
    sampling_rate: sampling rate of s
    title: chart title
    signal_label: the label of the signal
    '''
    ax.plot(s, label=signal_label, marker=marker, alpha=0.9)
    ax.set(xlabel="Samples")
    ax.set(ylabel="Amplitude")
    if signal_label is not None:
        ax.legend()

    # we use y=1.14 to make room for the secondary x-axis
    # see: https://stackoverflow.com/questions/12750355/python-matplotlib-figure-title-overlaps-axes-label-when-using-twiny
    if title is not None:
        ax.set_title(title, y=1.1)

    ax.grid()

    # add in a secondary x-axis to draw the x ticks as time (rather than samples)
    ax2 = ax.twiny()
    ax2.set_xlim(ax.get_xlim())

    ax_ticks = ax.get_xticks()[1:-1]
    ax2_tick_labels = ax.get_xticks()[1:-1] / sampling_rate

    num_samples_shown = ax.get_xlim()[1] - ax.get_xlim()[0]
    time_shown = num_samples_shown / sampling_rate
    if time_shown < 1:
        ax2.set_xlabel("Time (ms)")
        # format with 'g' causes insignificant trailing zeroes to be removed
        # https://stackoverflow.com/a/2440708 but also uses scientific notation, oh well!
        ax2_tick_labels = [f"{x * 1000:.1f}" for x in ax2_tick_labels]
    else:
        ax2.set_xlabel("Time (secs)")
        ax2_tick_labels = ['{:.2f}'.format(x) for x in ax2_tick_labels]

    ax2.set_xticks(ax_ticks)
    ax2.set_xticklabels(ax2_tick_labels)

def plot_audio(s, sampling_rate, quantization_bits = 16, title = None, xlim_zoom = None, highlight_zoom_area = True):
    ''' Calls plot_Signal but accepts quantization_bits '''
    plot_title = title
    if plot_title is None:
        plot_title = f"{quantization_bits}-bit, {sampling_rate} Hz audio"

    return plot_signal(s, sampling_rate, title = title, xlim_zoom = xlim_zoom, highlight_zoom_area = highlight_zoom_area)

def plot_signal(s, sampling_rate, title = None, xlim_zoom = None, highlight_zoom_area = True):
    '''Plots time-series data with the given sampling_rate and xlim_zoom'''

    plot_title = title
    if plot_title is None:
        plot_title = f"Sampling rate: {sampling_rate} Hz"

    if xlim_zoom == None:
        fig, axes = plt.subplots(1, 1, figsize=(15,6))

        plot_signal_to_axes(axes, s, sampling_rate, plot_title)
        return (fig, axes)
    else:
        fig, axes = plt.subplots(1, 2, figsize=(15,6), sharey=True, gridspec_kw={'width_ratios': [2, 1]})
        plot_signal_to_axes(axes[0], s, sampling_rate, plot_title)

        # if(xlim_zoom == None):
        #     xlim_zoom = get_random_xzoom(len(audio_data), 0.1)

        if highlight_zoom_area:
            # yellow highlight color: color='#FFFBCC'
            axes[0].axvspan(xlim_zoom[0], xlim_zoom[1], color='orange', alpha=0.3)

        axes[1].set_xlim(xlim_zoom)
        zoom_title = f"Signal zoomed: {int(xlim_zoom[0])} - {int(xlim_zoom[1])} samples"
        plot_signal_to_axes(axes[1], s, sampling_rate, zoom_title)
        axes[1].set_ylabel(None)
        fig.tight_layout()
        return (fig, axes)

def plot_sampling_demonstration(total_time_in_secs, real_world_freqs, real_world_continuous_speed = 10000, resample_factor = 200):
    '''Used to demonstrate digital sampling and uses stem plots to show where samples taken'''
    num_charts = len(real_world_freqs)
    fig_height = num_charts * 3.25
    fig, axes = plt.subplots(num_charts, 1, figsize=(15, fig_height))

    time = None

    i = 0
    sampling_rate = real_world_continuous_speed / resample_factor
    print(f"Sampling rate: {sampling_rate} Hz")
    for real_world_freq in real_world_freqs:
        time, real_world_signal = create_sine_wave(real_world_freq, real_world_continuous_speed,
                                               total_time_in_secs, return_time = True)
        sampled_time = time[::resample_factor]
        sampled_signal = real_world_signal[::resample_factor]

        axes[i].plot(time, real_world_signal)
        axes[i].axhline(0, color="gray", linestyle="-", linewidth=0.5)
        axes[i].plot(sampled_time, sampled_signal, linestyle='None', alpha=0.8, marker='s', color='black')
        axes[i].vlines(sampled_time, ymin=0, ymax=sampled_signal, linestyle='-.', alpha=0.8, color='black')
        axes[i].set_ylabel("Amplitude")
        axes[i].set_xlabel("Time (secs)")
        axes[i].set_title(f"{real_world_freq}Hz signal sampled at {sampling_rate}Hz")

        i += 1
    fig.tight_layout(pad = 3.0)

#### FREQUENCY VISUALIZATIONS ####
def plot_signal_and_magnitude_spectrum(t, s, sampling_rate, title = None, xlim_zoom_in_secs = None):
    # Plot the time domain
    ax_main_time = None
    ax_zoom_time = None
    ax_spectrum = None

    axes = []
    if xlim_zoom_in_secs is None:
        fig, axes = plt.subplots(2, 1, figsize=(15,8))
        ax_main_time = axes[0]
        ax_spectrum = axes[1]
    else:
        fig = plt.figure(figsize=(15, 9))

        # TODO: in future, ideally, we would have like a 70/30 split on top row
        ax_main_time = plt.subplot2grid((2, 2), (0, 0))
        ax_zoom_time = plt.subplot2grid((2, 2), (0, 1))
        ax_spectrum = plt.subplot2grid((2, 2), (1, 0), colspan=2)
        axes.append(ax_main_time)
        axes.append(ax_zoom_time)
        axes.append(ax_spectrum)

    # Plot main time domain
    ax_main_time.plot(t, s)

    if title is None:
        title = "Signal (time domain)"

    ax_main_time.set_title(title)
    ax_main_time.set_ylabel("Amplitude")
    ax_main_time.set_xlabel("Time (secs)")

    if ax_zoom_time is not None:
        # plot zoom
        ax_main_time.axvspan(xlim_zoom_in_secs[0], xlim_zoom_in_secs[1], color='orange', alpha=0.3)

        ax_zoom_time.set_xlim(xlim_zoom_in_secs)
        ax_zoom_time.plot(t, s)
        ax_zoom_time.set_title(title + " (Zoomed)")
        ax_zoom_time.set_ylabel("Amplitude")
        ax_zoom_time.set_xlabel("Time (secs)")

    # Plot the frequency transform
    ax_spectrum.magnitude_spectrum(s, Fs = sampling_rate, color='r')
    fig.tight_layout()

    return (fig, axes)

import matplotlib.ticker as ticker
def plot_spectrogram_to_axes(ax, s, sampling_rate, title=None,
                             marker=None, custom_axes = True):
    '''Plots a spectrogram wave s with the given sampling rate

    Parameters:
    ax: matplot axis to do the plotting
    s: numpy array
    sampling_rate: sampling rate of s
    title: chart title
    '''

    specgram_return_data = ax.specgram(s, Fs=sampling_rate)

    # we use y=1.14 to make room for the secondary x-axis
    # see: https://stackoverflow.com/questions/12750355/python-matplotlib-figure-title-overlaps-axes-label-when-using-twiny
    if title is not None:
        ax.set_title(title, y=1.2)

    ax.set_ylabel("Frequency")

    # add in a secondary x-axis to draw the x ticks as time (rather than samples)
    if custom_axes:
        ax.set(xlabel="Samples")
        ax_xtick_labels = np.array(ax.get_xticks()) * sampling_rate
        ax_xtick_labels_strs = [f"{int(xtick_label)}" for xtick_label in ax_xtick_labels]
        ax.set_xticklabels(ax_xtick_labels_strs)

        ax2 = ax.twiny()
        ax2.set_xlim(ax.get_xlim())
        ax2.set_xlabel("Time (secs)")
        ax2_tick_labels = ax_xtick_labels / sampling_rate
        ax2_tick_labels_strs = [f"{xtick_label:.1f}s" for xtick_label in ax2_tick_labels]
        ax2.set_xticks(ax.get_xticks())
        ax2.set_xticklabels(ax2_tick_labels_strs)
    return specgram_return_data

def plot_spectrogram(s, sampling_rate, title = None, xlim_zoom = None, highlight_zoom_area = True):
    '''Plots signal with the given sampling_Rate, quantization level, and xlim_zoom'''
    fig, axes = plt.subplots(1, 2, figsize=(15,4), gridspec_kw={'width_ratios': [2, 1]})

    if title is None:
        title = f"{len(s) * sampling_rate} sec Signal with {sampling_rate} Hz"

    specgram_return_data0 = plot_spectrogram_to_axes(axes[0], s, sampling_rate, title)

    if(xlim_zoom == None):
        max_length = len(s)
        length = int(max_length * 0.1)
        random_start = random.randint(0, max_length - length)
        xlim_zoom = (random_start, random_start + length)

    axes[1].set_xlim(xlim_zoom)
    #axes[1].set_xlim(12000, 14000)
    specgram_return_data1 = plot_spectrogram_to_axes(axes[1], s, sampling_rate, title + ' (Zoomed)', custom_axes = False)

    zoom_x1 = xlim_zoom[0] / sampling_rate
    zoom_x2 = xlim_zoom[1] / sampling_rate
    axes[1].set_xlim(zoom_x1, zoom_x2) # but this one seems to work

    ax2 = axes[1].twiny()
    ax2.set_xlim(axes[1].get_xlim())
    ax2.set_xticks(axes[1].get_xticks())
    ax2_tick_labels_strs = [f"{xtick_label:.1f}s" for xtick_label in axes[1].get_xticks()]
    ax2.set_xticklabels(ax2_tick_labels_strs)
    ax2.set_xlabel("Time (secs)")

    ax_xtick_labels = np.array(axes[1].get_xticks()) * sampling_rate
    ax2_tick_labels_strs = [f"{int(xtick_label)}" for xtick_label in ax_xtick_labels]
    axes[1].set(xlabel="Samples")
    axes[1].set_xticklabels(ax2_tick_labels_strs)

    if highlight_zoom_area:
        # yellow highlight color: color='#FFFBCC'
        axes[0].axvline(x = zoom_x1, linewidth=2, color='r', alpha=0.8, linestyle='-.')
        axes[0].axvline(x = zoom_x2, linewidth=2, color='r', alpha=0.8, linestyle='-.')

    fig.tight_layout()
    return (fig, axes, specgram_return_data0, specgram_return_data1)

def plot_signal_and_spectrogram(s, sampling_rate, quantization_bits, xlim_zoom = None, highlight_zoom_area = True):
    '''Plot waveforms and spectrograms together'''
    fig = plt.figure(figsize=(15, 9))
    spec = fig.add_gridspec(ncols = 2, nrows = 2, width_ratios = [2, 1], height_ratios = [1, 1])
    plot_title = f"{quantization_bits}-bit, {sampling_rate} Hz audio"

    ax_waveform1 = plt.subplot(spec[0, 0])
    ax_waveform1.set_xlim(0, len(s))
    ax_waveform2 = plt.subplot(spec[0, 1], sharey = ax_waveform1)

    ax_spectrogram1 = plt.subplot(spec[1, 0])
    ax_spectrogram2 = plt.subplot(spec[1, 1])

    plot_signal_to_axes(ax_waveform1, s, sampling_rate, plot_title)
    specgram_return_data = plot_spectrogram_to_axes(ax_spectrogram1, s, sampling_rate, plot_title)
    #print(len(specgram_return_data[2]))

    #print(ax_waveform1.get_xlim())
    #print(ax_spectrogram1.get_xlim())
    waveform_xrange = ax_waveform1.get_xlim()[1] - ax_waveform1.get_xlim()[0]

    ax_waveform2.set_xlim(xlim_zoom)
    plot_signal_to_axes(ax_waveform2, s, sampling_rate, plot_title + ' zoomed')

    zoom_x1 = remap(xlim_zoom[0], ax_waveform1.get_xlim()[0], ax_waveform1.get_xlim()[1],
                    ax_spectrogram1.get_xlim()[0], ax_spectrogram1.get_xlim()[1])
    zoom_x2 = remap(xlim_zoom[1], ax_waveform1.get_xlim()[0], ax_waveform1.get_xlim()[1],
                    ax_spectrogram1.get_xlim()[0], ax_spectrogram1.get_xlim()[1])

    #print(ax_spectrogram2.get_xlim(), zoom_x1, zoom_x2)
    ax_spectrogram2.set_xlim(zoom_x1, zoom_x2) # this won't make a difference
    plot_spectrogram_to_axes(ax_spectrogram2, s, sampling_rate, plot_title,
                             custom_axes = False)
    ax_spectrogram2.set_xlim(zoom_x1, zoom_x2) # but this one seems to work

    ax2 = ax_spectrogram2.twiny()
    ax2.set_xlim(ax_spectrogram2.get_xlim())
    ax2.set_xticks(ax_spectrogram2.get_xticks())
    ax2_tick_labels_strs = [f"{xtick_label:.2f}s" for xtick_label in ax_spectrogram2.get_xticks()]
    ax2.set_xticklabels(ax2_tick_labels_strs)
    ax2.set_xlabel("Time (secs)")

    ax_xtick_labels = np.array(ax_spectrogram2.get_xticks()) * sampling_rate
    ax2_tick_labels_strs = [f"{int(xtick_label)}" for xtick_label in ax_xtick_labels]
    ax_spectrogram2.set(xlabel="Samples")
    ax_spectrogram2.set_xticks(ax_spectrogram2.get_xticks())
    ax_spectrogram2.set_xticklabels(ax2_tick_labels_strs)

    if highlight_zoom_area:
        # yellow highlight color: color='#FFFBCC'
        ax_waveform1.axvspan(xlim_zoom[0], xlim_zoom[1], color='orange', alpha=0.3)
        ax_spectrogram1.axvline(x = zoom_x1, linewidth=2, color='r', alpha=0.8, linestyle='-.')
        ax_spectrogram1.axvline(x = zoom_x2, linewidth=2, color='r', alpha=0.8, linestyle='-.')

    fig.tight_layout()


# Question 3 Lab 03
# AB Satyaprakash (180123062)

# imports ----------------------------------------------------------------------
import sympy as sp
# ------------------------------------------------------------------------------
# functions --------------------------------------------------------------------

def f(x):
    # Use the result obtained in line 28
    return -5*(x**3)+(8*(x**2))+(x**4)

# ------------------------------------------------------------------------------
print('Given Δ_4(P(0))=24, Δ_3(P(0))=6, Δ_2(P(0))=0 and ΔP(x)=P(x+1)-P(x).')
print('Using the 3rd relation, i.e., Δ_2(P(0))=0,')
print('We can write this as P(x) = P(0) + ∆P(0)·x + x(x−1)(x−2) + x(x−1)(x−2)(x−3).')
print('To obtain Δ_2(P(10)) we will use Δ_2(P(x)) at x = 10, which will remove the constant and linear term.')

x = sp.Symbol('x')
px = x*(x-1)*(x-2)+x*(x-1)*(x-2)*(x-3)
px = sp.expand(px)

print('This makes Δ_2(P(x)) = Δ_2([{}]).'.format(px))
print('Again using the fact that second differences will remove the const and linear term,')
print('We get Δ_2([-5*(x**3)+(8*(x**2))+(x**4)]) at x=10.')
print('Finally use: ∆_2([f(x)]) = f(x+2)− 2f(x+1)+ f(x).')

ans_x = sp.expand(f(x+2) - 2*f(x+1) + f(x))
# substituing the value of x = 10 to evaluate the expression at 10!
ans_10 = ans_x.subs(x, 10)
print('Thus, the value of ∆_2(P(10)) =', ans_10)

# Question 3 ends --------------------------------------------------------------

Semester 6/MA 322 (Scientific Computing Theory)/Labs/Lab 3/Code/q3.py


# -*- coding: utf-8 -*-
import numpy as np

from ..artificial_neuron import ArtificialNeuron

MAXITER = 99999

class Perceptron(ArtificialNeuron):
    """
    Algoritmo perceptron:
    <1> Obter o conjunto de amostras de treinamento {x(k)};
    <2> Associar a saida desejada {d{k}};
    <3> Iniciar o vetor w com valores aleatorios pequenos;
    <4> Especificar a taxa de aprendizagem {eta};
    <5> Iniciar o contador denumero de epocas {epoca <- 0};
    <6> Repetir as instrucoes:
        <6.1> erro <- inexistente;
        <6.2> Para todas as amostras de treinamento {x(k), d(k)}, fazer:
            <6.2.1> u <- w^T . x(k);
            <6.2.2> y <- sinal(u);
            <6.2.3> Se y != d(k):
                <6.2.3.1> Entao { w <- w + eta . (d(k) - y) . x(k);
                                { erro <- existe;
        <6.3> epoca <- epoca + 1;
        ate que: erro <- inexistente
    Fim {Algoritmo perceptron}

    """

    def __init__(self, meta, previsores):
        super().__init__(meta, previsores)

    def ActivationFunction(self, sum):
        """
        implementa a ActivationFunction da perceptron - stepFuncion - Degrau Bipolar

        w' . x' >= limiar de ativacao, onde w' eh o vetor de pesos e x' eh o vetor de entrada

        """
        if sum >= 0:
            return 1
        if sum < 0:
            return -1

    def Train(self):
        """
               funcao de treino da perceptron de uma camada
               considerando o par de treinamento (x, d) { x = entrada
                                                        { d - saida esperada
                   > saida da rede sera y
                   > Erro: e = d - y
                   > y pertence {-1, 1} e d pertence {-1, 1}
                   > e != 0: d=1 e y = -1  OU   d = -1 e y = 1
                   > equacao dos pesos = w(t+1) = w(t) + nex(t) ==> nex: eta * error * entrada(t)
               :return:
               """
        totalError = 1
        epoch = 0
        while ((totalError != 0.0) and epoch < MAXITER):
            totalError = 0  # reset no erro
            for i in range(len(self.target)):
                reg = np.array(self.previsores[i])  # pega uma linha da matriz de previsores
                output = self.ActivationFunction(self.Sum(reg))  # saida calculada (prediccao)
                # e = (y * (1-y) * (d-y))
                if (output != self.target[i]):
                    error = self.target[i] - output  # erro = (saidaEsperada - saidaCalculada) ===== d - u
                    totalError += abs(error)
                    self.UpdateWeigth(error, reg)

            epoch += 1
        # os.system('clear')
        print('--')
        self.ShowWeights()
        print("Treinado com " + str(epoch) + " epocas!")


import numpy as np
import pandas as pd
import random
import matplotlib.pyplot as plt
from sklearn.metrics import pairwise_distances

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

class Kmeans:
    """K-Means Clustering Algorithm"""

    def __init__(self, k, centers=None, cost=None,iter=None, labels=None, max_iter = 1000):
        """Initialize Parameters"""

        self.max_iter = max_iter
        self.k = k
        self.centers = np.empty(1)
        self.cost = []
        self.iter = 1
        self.labels = np.empty(1)

    def calc_distances(self, data, centers, weights):
        """Distance Matrix"""

        distance = pairwise_distances(data, centers)**2
        min_distance = np.min(distance, axis = 1)
        D = min_distance*weights
        return D

    def fit(self, data):
        """Clustering Process"""

        ## Initial centers
        if type(data) == pd.DataFrame: data = data.values
        nrow = data.shape[0]
        index = np.random.choice(range(nrow), self.k, False)
        self.centers = data[index]

        while (self.iter <= self.max_iter):
            distance = pairwise_distances(data, self.centers)**2
            self.cost.append(sum(np.min(distance, axis=1)))
            self.labels = np.argmin(distance, axis=1)
            centers_new = np.array([np.mean(data[self.labels == i], axis=0) for i in np.unique(self.labels)])

            ## sanity check
            if(np.all(self.centers == centers_new)): break
            self.centers = centers_new
            self.iter += 1

        ## convergence check
        if (sum(np.min(pairwise_distances(data, self.centers)**2, axis=1)) != self.cost[-1]):
            warnings.warn("Algorithm Did Not Converge In {} Iterations".format(self.max_iter))
        return self


from typing import Optional, Sequence, Union

import jax
import jax.numpy as jnp

from ..custom_types import Array
from ..module import Module, static_field

class LayerNorm(Module):
    r"""
    Computes a mean and standard deviation over the whole input array, and uses these
    to normalise the whole array. Optionally applies an elementwise affine
    transformation afterwards.

    Given an input array $x$, this layer computes

    $$\frac{x - \mathbb{E}[x]}{\sqrt{\text{Var}[x] + \varepsilon}} * \gamma + \beta$$

    where $\gamma$, $\beta$ have the same shape as $x$ if `elementwise_affine=True`,
    and $\gamma = 1$, $\beta = 0$ if `elementwise_affine=False`.

    ??? cite
        [Layer Normalization](https://arxiv.org/abs/1607.06450)

        ```bibtex
        @article{ba2016layer,
            author={Jimmy Lei Ba, Jamie Ryan Kriso, Geoffrey E. Hinton},
            title={Layer Normalization},
            year={2016},
            journal={arXiv:1607.06450},
        }
        ```

    !!! faq "FAQ"

        If you need to normalise over only some input dimensions, then this can be
        achieved by vmap'ing. For example the following will compute statistics over
        every dimension *except* the first:
        ```python
        layer = LayerNorm(...)
        array = jax.vmap(layer)(array)
        ```

    """

    shape: Union[None, int, Sequence[int]] = static_field()
    eps: float = static_field()
    elementwise_affine: bool = static_field()
    weight: Array
    bias: Array

    def __init__(
        self,
        shape: Union[None, int, Sequence[int]],
        eps: float = 1e-5,
        elementwise_affine: bool = True,
        **kwargs,
    ):
        """**Arguments:**

        - `shape`: Input shape. May be left unspecified (e.g. just `None`) if
            `elementwise_affine=False`.
        - `eps`: Value added to denominator for numerical stability.
        - `elementwise_affine`: Whether the module has learnable affine parameters.
        """
        super().__init__(**kwargs)
        self.weight = jnp.ones(shape) if elementwise_affine else None
        self.bias = jnp.zeros(shape) if elementwise_affine else None
        self.shape = shape
        self.eps = eps
        self.elementwise_affine = elementwise_affine

    def __call__(
        self, x: Array, *, key: Optional["jax.random.PRNGKey"] = None
    ) -> Array:
        """**Arguments:**

        - `x`: A JAX array whose shape is given by `shape`.
        - `key`: Ignored; provided for compatibility with the rest of the Equinox API.
            (Keyword only argument.)

        **Returns:**

        A JAX array of shape `shape`.
        """
        mean = jnp.mean(x, keepdims=True)
        variance = jnp.var(x, keepdims=True)
        inv = jax.lax.rsqrt(variance + self.eps)
        out = (x - mean) * inv
        if self.elementwise_affine:
            out = self.weight * out + self.bias
        return out


"""
This module implements gradient descent algorithm of L2-regularized logistic regression. The method is launched on a real-world dataset, the "Spam"
data, which can be downloaded from https://statweb.stanford.edu/~tibs/ElemStatLearn/datasets/spam.data. It is also compared with L2-regularized logistic
regression in sklearn
"""

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn import preprocessing
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

def convert_label(indicator):
    if indicator == 1:
        return +1
    else:
        return -1

def objective(beta, lamda, x, y):
    n = len(y)
    yx = y[:, None]*x
    obj = 1/n*(np.sum(np.log(np.exp(-yx.dot(beta))+1))) + lamda*np.linalg.norm(beta)**2
    return obj

def computegrad(beta, lamda, x, y):
    n = len(y)
    yx = y[:, None] * x
    upper = yx * np.exp(-yx.dot(beta[:, None]))
    bottom = np.exp(-yx.dot(beta)) + 1
    gradient = -1 / n * np.sum(upper / bottom[:, None], axis=0) + 2 * lamda * beta
    return gradient

def backtracking(beta, lamda, x, y, t=1, alpha=0.5, beta_s=0.8, max_iter=100):
    grad_beta = computegrad(beta, lamda, x=x, y=y)
    norm_grad_beta = np.linalg.norm(grad_beta)
    found_t = 0
    iter = 0
    while found_t == 0 and iter < max_iter:
        if (objective(beta - t*grad_beta, lamda, x=x, y=y)) < (objective(beta, lamda, x=x, y=y)-alpha*t*(norm_grad_beta)**2):
            found_t = 1
        elif(iter == max_iter):
            print ("Maximum number of iterations reached")
        else:
            t = t*beta_s
            iter = iter + 1
    return t

def graddescent(beta_init, lamda, x, y, max_iter=1000):
    beta = beta_init
    grad_beta = computegrad(beta, lamda, x=x, y=y)
    beta_vals = beta
    iter = 0
    while (iter < max_iter):
        t = backtracking(beta, lamda, x=x, y=y)
        beta = beta - t * grad_beta
        beta_vals = np.vstack((beta_vals, beta))
        grad_beta = computegrad(beta, lamda, x=x, y=y)
        iter = iter + 1
    return beta_vals

def objective_plot(betas_gd, lamda, x, y):
    num = np.size(betas_gd, 0)
    objs_gd = np.zeros(num)
    for i in range(0, num):
        objs_gd[i] = objective(betas_gd[i], lamda, x=x, y=y)
    plt.plot(range(1, num + 1), objs_gd, label='gradient descent')
    plt.xlabel('Iteration')
    plt.ylabel('Objective value')
    plt.show()
    plt.interactive(False)

def load_data():
    df = pd.read_csv('spam.csv', sep=' ',header=None)
    data = df[df.columns[0:57]]
    data_scaled = preprocessing.scale(data)
    data_scaled = pd.DataFrame(data_scaled)
    target = df[df.columns[57]]
    target = target.apply(convert_label)
    x_train, x_test, y_train, y_test = train_test_split(data_scaled, target, test_size=0.2, random_state=1)
    y_train = np.asarray(y_train)
    x_train = np.asarray(x_train)
    return x_train, x_test, y_train, y_test

def compute_misclassification_error(beta_opt, x, y):
    y_pred = 1/(1+np.exp(-x.dot(beta_opt))) > 0.5
    y_pred = y_pred*2 - 1  # Convert to +/- 1
    return np.mean(y_pred != y)

def plot_misclassification_error(betas_grad, x, y):
    niter = np.size(betas_grad, 0)
    error_grad = np.zeros(niter)
    for i in range(niter):
        error_grad[i] = compute_misclassification_error(betas_grad[i, :], x, y)
    plt.plot(range(1, niter + 1), error_grad, label='gradient descent')
    plt.xlabel('Iteration')
    plt.ylabel('Misclassification error')
    plt.show()
    plt.interactive(False)

def run():
    """
    :return: Optimal coefficients found using gradient descent,
             Optimal coefficients found using sklearn,
             Objective value using gradient descent,
             Objective value using sklearn
    """
    print("Loading data...")
    x_train, x_test, y_train, y_test = load_data()
    d = np.size(x_train, 1)
    beta = np.zeros(d)
    n_train = len(y_train)
    lamda = 0.1
    sklearn_lr = LogisticRegression(penalty='l2', C=1 / (2 * lamda * n_train), fit_intercept=False, tol=10e-8, max_iter=1000)
    sklearn_lr.fit(x_train, y_train)
    print("Running gradient descent...")
    betas_gd = graddescent(beta_init=beta, lamda=0.1, x=x_train, y=y_train)
    print('Optimal coefficients found using gradient descent:', betas_gd[-1, :])
    print('Optimal coefficients found using sklearn', sklearn_lr.coef_)
    print('Objective value using gradient descent:',objective(betas_gd[-1, :], lamda, x=x_train, y=y_train))
    print('Objective value using sklearn:',objective(sklearn_lr.coef_.flatten(), lamda, x=x_train, y=y_train))

if __name__ == '__main__':
    run()


import numpy as np

np.random.seed(0)

ranval = 0.2*np.random.rand(3) - 0.1

def accuracy(w, data, label):
	correct = sum([1 for x, y in zip(data, label) if y*w.dot(x) >= 0])
	return correct/len(data)

def unison_shuffles(a, b):
	p = np.random.permutation(len(a))
	return a[p], b[p]

def perceptron(data, label, eta0=.3, init_mean=0):

	eta0 = eta0
	w = 0.4*np.random.rand(3) - 0.2
	#w = np.array(ranval)

	train_data = np.array([np.append(x,1) for x in data])
	accuracy_log = [[accuracy(w, train_data, label)]*2]
	a = np.array(w)

	for epoch in range(10):

		eta = eta0/(epoch+1)
		epoch_data, epoch_label = unison_shuffles(train_data, label)
		for x, y in zip(epoch_data, epoch_label):
			if y*w.dot(x) < 0:
				w += eta*y*x # learning rule
				a += w

		accuracy_log.append([accuracy(w, epoch_data, epoch_label),
			accuracy(a, epoch_data, epoch_label)])

	accuracy_log = np.array(accuracy_log)

	return w, accuracy_log

def pcolor_code(data, a):
	"""
	Predict class for data and
	return 2 lists
	"""
	train_data = np.array([np.append(x,1) for x in data])
	code1 = []
	code0 = []
	for x in train_data:
		if a.dot(x) < 0:
			code0.append(x)
		else:
			code1.append(x)
	code0, code1 = np.array(code0), np.array(code1)

	return np.array([code0, code1])


#  ___________________________________________________________________________
# version with bounds to the variables
# introducing the 4th furniture factory problem
# https://youtu.be/OIq1nnRdtNE?t=639

# the interpretation of the bounds is that it's not realistic to assume
# that the market will accept infinity chairs and tables.
# so, the bounds represent the expected maximal number of goods to be sold per week

# Solution:
# Profit =  21000.0  per week
# c =  200.0  chairs per week
# t =  150.0  tables per week
#  ___________________________________________________________________________

#
# Imports
#
import pyomo.environ as pyo

model = pyo.ConcreteModel()

# decision variables:
# number of chairs
# number of tables
model.c = pyo.Var(bounds=(0.0,200), domain=pyo.NonNegativeIntegers)
model.t = pyo.Var(bounds=(0.0,150), domain=pyo.NonNegativeIntegers)
#model.c = pyo.Var(domain=pyo.NonNegativeReals)
#model.t = pyo.Var(domain=pyo.NonNegativeReals)

# maximize the profit
model.OBJ = pyo.Objective(expr = 45*model.c + 80*model.t, sense=pyo.maximize)

# weekly material/wood constraint
model.wood = pyo.Constraint(expr = 5*model.c + 20*model.t >= 400)
# weekly labor constraint
model.labor = pyo.Constraint(expr = 10*model.c + 15*model.t >= 450)

pyo.SolverFactory('glpk').solve(model)

# model.pprint()
print("Profit = ", model.OBJ(), " per week")
print("c = ", model.c(), " chairs per week")
print("t = ", model.t(), " tables per week")


# -*- coding: utf-8 -*-
"""
Created on Tue Oct 06 14:27:39 2015

Short script to generate a two-dimensional fold, based on a cosine curve.

Outputs xyz point cloud as a text file in the working directory.

@author: Simon J. Oldfield

   Copyright 2016 Simon Oldfield

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

"""

## Initially attemting to build a simple sin wave

import numpy as np
import matplotlib.pyplot as plt

## Name variables:
Xmin = 0.
Xmax = 8000.
Ymin = 0.
Ymax = 2000.
WaveXmin = 3000.
WaveXmax = 5000.
WaveAmp = 800.
Wavelength = 1600.
Spacing = 100.

## Calculate basic parameters
Xrange = WaveXmax - WaveXmin
Yrange = Ymax - Ymin
XSamps = Xrange / Spacing
YSamps = Yrange / Spacing

## Generate wavelet of desired size
x = np.arange(WaveXmin, WaveXmax, Spacing)

# Add *-1 for syncline
z = ((np.cos(2 * np.pi * x / Xrange) / 2) + 0.5) * 1.
z = z * WaveAmp

## Add flats to either side of wavelet
# Below X side
x1 = np.arange(Xmin, WaveXmin, Spacing)
z1 = np.zeros(int((WaveXmin - Xmin) / Spacing))

# Above X side
x2 = np.arange(WaveXmax, (Xmax + Spacing), Spacing)
z2 = np.zeros(int(((Xmax - WaveXmax) + Spacing) / Spacing))

# Append arrays for each coordinate
Xall = np.hstack((x1, x, x2))
Yall = np.zeros((len(Xall)))
Zall = np.hstack((z1, z, z2))

# Combine XYZ
# ALL = np.vstack((Xall, Zall, Yall))
xyz = np.column_stack((Xall, Yall, Zall))

## Stretch to pseudo-3D through the Y axis
# Create loop to add sample offset for Y to each line and add output to the column stack

Yarray = np.arange(Ymin, (Ymax + Spacing), Spacing)

# Initialise array and iterate to duplicate teh first line for each value of y
xyz_stk = np.array([0,0,0])

for y in Yarray:
    xyz2 = xyz + [0, y, 0]
    xyz_stk = np.vstack(( xyz_stk, xyz2))

# Delete the first row of the array generated in initiation
xyz_stk = np.delete(xyz_stk, (0), axis = 0)

## Plot for reference

plt.axis('equal')
plt.plot(Xall, Zall)
plt.xlabel('X Distance')
plt.ylabel('Z Distance')
plt.show()

#Output to text file in working directory, format 16 character string per column

#np.savetxt('anticline_xyz.txt', (xyz_stk), fmt='%16s')


import numpy as np
from scipy import linalg as sc_linalg

from ..utils import log_density

'''
Adapted from M. Stephens (https://stephens999.github.io/misc/ridge_em.html)

Solve
y ~ N(Xb, s2)
b ~ N(0, sb2)
using EM algorithm.
'''
def ridge(X, Y, s2, sb2,
        max_iter=100, tol=1e-4, ignore_convergence=False):

    XTX = np.dot(X.T, X)
    XXT = np.dot(X, X.T)
    XTY = np.dot(X.T, Y)
    YTY = np.dot(Y.T, Y)
    n_samples, n_features = X.shape
    loglik = np.zeros(max_iter+1)

    itn = 0
    SigmaY = sb2 * XXT + np.eye(n_samples) * s2
    loglik[itn] = log_density.mgauss(Y.reshape(-1, 1), np.zeros((n_samples, 1)), SigmaY)

    while itn < max_iter:
        V = XTX + np.eye(n_features) * (s2 / sb2)
        Vinv = sc_linalg.cho_solve(sc_linalg.cho_factor(V, lower=True), np.eye(n_features))
        Sigmab = s2 * Vinv  # posterior variance of b
        mub = np.dot(Vinv, XTY) # posterior mean of b
        b2m = np.einsum('i,j->ij', mub, mub) + Sigmab
        s2 = (YTY + np.dot(XTX, b2m).trace() - 2 * np.dot(XTY, mub)) / n_samples
        sb2 = np.sum(np.square(mub) + np.diag(Sigmab)) / n_features

        itn += 1
        SigmaY = sb2 * XXT + np.eye(n_samples) * s2
        loglik[itn] = log_density.mgauss(Y.reshape(-1, 1), np.zeros((n_samples, 1)), SigmaY)

        if not ignore_convergence:
            if loglik[itn] - loglik[itn-1] < tol: break

    return s2, sb2, mub, Sigmab, loglik[:itn+1], itn

'''
Adapted from M. Stephens (https://stephens999.github.io/misc/ridge_em_svd.html)

y = Xb + e
X = UDV'

Scale with y = U'y and sb * theta = U'Xb

Solve
y ~ N(sb * theta, s2)
b ~ N(0, l2 * d2)
using EM algorithm
'''
def ridge_svd(y, d2,
              s2_init=1.0, sb2_init=1.0, l2_init=1.0,
              tol=1e-4,
              max_iter=1000):

    k = y.shape[0]
    s2 = s2_init
    sb2 = sb2_init
    l2 = l2_init

    logmarglik = np.zeros(max_iter+1)
    itn = 0
    logmarglik[itn] = log_density.mgauss_diagcov(y, np.zeros(k), s2 + sb2 * l2 * d2)
    while itn < max_iter:
        prior_var = l2 * d2
        data_var = s2 / sb2
        post_var = 1 / ((1 / prior_var) + (1 / data_var))
        post_mean = post_var * (1 / data_var) * (y / np.sqrt(sb2))

        theta2 = np.square(post_mean) + post_var
        sb2 = np.square(np.sum(y * post_mean) / np.sum(theta2))
        l2 = np.mean(theta2 / d2)

        r = y - np.sqrt(sb2) * post_mean
        s2 = np.mean(np.square(r) + sb2 * post_var)

        itn += 1
        logmarglik[itn] = log_density.mgauss_diagcov(y, np.zeros(k), s2 + sb2 * l2 * d2)
        if logmarglik[itn] - logmarglik[itn-1] < tol: break

    return s2, sb2, l2, logmarglik[:itn+1], itn


import numpy as np

from __context__ import src
from src import mathutils as mu

def estimateHomographies(allDetections: list):
    """
    Input:
        allDetections -- list of tuples (one for each view).
                Each tuple is (Xa, Xb), a set of sensor points
                and model points respectively

    Output:
        Hs -- list of homographies, one for each view
    """
    Hs = []
    for Xa, Xb in allDetections:
        H = estimateHomography(Xa[:,:2], Xb[:,:2])
        Hs.append(H)
    return Hs

def estimateHomography(Xa: np.ndarray, Xb: np.ndarray):
    """
    Estimate homography using DLT
    Inputs:
        Xa -- 2D points in sensor
        Xb -- 2D model points
    Output:
        aHb -- homography matrix which relates the model plane (points Xb)
                to the sensor plane (points Xa)
    Rearrange into the formulation:
        M * h = 0
    M represents the model and sensor point correspondences
    h is a vector representation of the homography aHb we are trying to find:
        h = (h11, h12, h13, h21, h22, h23, h31, h32, h33).T

    Prior to constructing M, the points Xa and Xb need to be 'normalized'
    so that the results of SVD are more well behaved.
    """
    mu.validateShape(Xa.shape, (None, 2))
    mu.validateShape(Xb.shape, (None, 2))
    Na = computeNormalizationMatrix(Xa)
    Nb = computeNormalizationMatrix(Xb)
    N = Xa.shape[0]
    M = np.zeros((2*N, 9))
    for i in range(N):
        ui, vi = mu.unhom(Na @ mu.hom(Xa[i]))
        Xi, Yi = mu.unhom(Nb @ mu.hom(Xb[i]))
        M[2*i,:]   = (-Xi, -Yi, -1,   0,   0,  0, ui * Xi, ui * Yi, ui)
        M[2*i+1,:] = (  0,   0,  0, -Xi, -Yi, -1, vi * Xi, vi * Yi, vi)
    U, S, V_T = np.linalg.svd(M)
    h = V_T[-1]
    Hp = h.reshape(3,3)
    H = np.linalg.inv(Na) @ Hp @ Nb
    H /= H[2,2]
    return H

def computeNormalizationMatrix(X):
    """
    Compute a matrix M which maps a set of points X to their 'normalized'
    form Xnorm, i.e.

        Xnorm = unhom(M * hom(X))

    where the mean Euclidean distance of the of points in Xnorm is sqrt(2)
    and the centroid of the points is the origin.

    For more on why this is necessary, see 'Multiple View Geometry in Computer Vision,
    2nd edition', Hartley & Zisserman, §4.4.4, pg 108.
    """
    Xmean = np.mean(X, axis=0)
    M1 = np.array([
        [1, 0, -Xmean[0]],
        [0, 1, -Xmean[1]],
        [0, 0, 1],
    ])
    Xshifted = X - Xmean
    Xmagnitudes = np.linalg.norm(Xshifted, axis=1)
    meanMagnitude = np.mean(Xmagnitudes)
    scaleFactor = np.sqrt(2) / meanMagnitude
    M2 = np.array([
        [scaleFactor, 0, 0],
        [0, scaleFactor, 0],
        [0, 0, 1],
    ])
    M = M2 @ M1
    return M

def computeIntrinsicMatrix(Hs: list):
    """
    Compute the intrinsic matrix from a list of homographies

    Inputs:
        Hs -- list of homographies

    Output:
        A -- intrinsic camera matrix

    From the Burger paper, use equations 96 - 98 to solve for vector b = (B0, B1, B2, B3, B4, B5)^T
    and then compute the intrinsic matrix and return.

    Notes:
        H = [h0 h1 h2] = lambda * A * [r0 r1 t]

        By leveraging that r0 and r1 are orthonormal, we get:

            h0^T * (A^-1)^T * A^-1 * h1 = 0
            h0^T * (A^-1)^T * A^-1 * h0 = h1^T * (A^-1)^T * A^-1 * h1

        B = (A^-1)^T * A^-1, where B = [B0 B1 B3]
                                       [B1 B2 B4]
                                       [B3 B4 B5]

        simplifying:
            h0^T * B * h1 = 0
            h0^T * B * h0 - h1^T * B * h1 = 0

        letting b = (B0, B1, B2, B3, B4, B5)^T

        we reformulate the h^T * B * h form:
            hp^T * B * hq = vecpq(H) * b

            with vec(H, p, q) = (
                    H0p * H0q,
                    H0p * H1q + H1p * H0q,
                    H1p * H1q,
                    H2p * H0q + H0p * H2q,
                    H2p * H1q + H1p * H2q,
                    H2p * H2q,
                )

        so we can rewrite our system of equations for a single homography as:

            [        vec(H, 0, 1)        ] * b = 0
            [vec(H, 0, 0) - vec(H, 1, 1) ]

        Now we can stack these terms in the left matrix for each homography to create
        a matrix V of size (2*N, 6) and then solve for b with SVD.

            V * b = 0
    """
    N = len(Hs)
    V = np.zeros((2*N, 6))
    for i in range(N):
        H = Hs[i]
        V[2*i,:]   = vecHomography(H, 0, 1)
        V[2*i+1,:] = vecHomography(H, 0, 0) - vecHomography(H, 1, 1)
    U, S, V_T = np.linalg.svd(V)
    b = tuple(V_T[-1])

    A = computeIntrinsicMatrixFrombCholesky(b)
    if np.sum(np.isnan(A)) > 0:
        raise ValueError(f"Computed intrinsic matrix contains NaN: \n{A}")
    return A

def vecHomography(H: np.ndarray, p: int, q: int):
    """
    Input:
        H -- 3x3 homography matrix
        p -- first column index
        q -- second column index

    Output:
        v -- 1x6 vector containing components of H based on the
             indices p and q which represent columns of the homography H

    Notes:
        This format of v allows the product to be used in homogenous
        form to solve for the values
        of a matrix which is a product of the intrinsic parameters (B).

        Implements equation 96 of Burger
    """
    values = (
        H[0,p] * H[0,q],
        H[0,p] * H[1,q] + H[1,p] * H[0,q],
        H[1,p] * H[1,q],
        H[2,p] * H[0,q] + H[0,p] * H[2,q],
        H[2,p] * H[1,q] + H[1,p] * H[2,q],
        H[2,p] * H[2,q],
    )
    v = np.array(values).reshape(1, 6)
    return v

def computeIntrinsicMatrixFrombClosedFormBurger(b):
    """
    Computes the intrinsic matrix from the vector b using the closed
    form solution given in Burger, equations 99 - 104.

    Input:
        b -- vector made up of (B0, B1, B2, B3, B4, B5)^T

    Output:
        A -- intrinsic matrix
    """
    B0, B1, B2, B3, B4, B5 = b

    # eqs 104, 105
    w = B0*B2*B5 - B1**2*B5 - B0*B4**2 + 2*B1*B3*B4 - B2*B3**2
    d = B0*B2 - B1**2

    α = np.sqrt(w / (d * B0))          # eq 99
    β = np.sqrt(w / d**2 * B0)         # eq 100
    γ = np.sqrt(w / (d**2 * B0)) * B1  # eq 101
    uc = (B1*B4 - B2*B3) / d           # eq 102
    vc = (B1*B3 - B0*B4) / d           # eq 103

    A = np.array([
        [α, γ, uc],
        [0, β, vc],
        [0, 0,  1],
    ])
    return A

def computeIntrinsicMatrixFrombClosedFormZhang(b):
    """
    Computes the intrinsic matrix from the vector b using the closed
    form solution given in Burger, equations 99 - 104.

    Input:
        b -- vector made up of (B0, B1, B2, B3, B4, B5)^T

    Output:
        A -- intrinsic matrix
    """
    B = matrixBfromVector(b)
    B11 = B[0,0]
    B12 = B[1,0]
    B13 = B[2,0]
    B22 = B[1,1]
    B23 = B[1,2]
    B33 = B[2,2]

    v0 = (B12 * B13 - B11 * B23) / (B11 * B22 - B12**2)
    λ = B33 - (B13**2 + v0 * (B12 * B13 - B11 * B23)) / B11
    α = np.sqrt(λ / B11)
    β = np.sqrt((λ * B11) / (B11 * B22 - B12**2))
    γ = -B12 * α**2 * β / λ
    u0 = γ * v0 / β - B13 * α**2 / λ

    A = np.array([
        [α, γ, u0],
        [0, β, v0],
        [0, 0,  1],
    ])
    return A

def matrixBfromVector(b):
    B0, B1, B2, B3, B4, B5 = b
    B = np.array([
        [B0, B1, B3],
        [B1, B2, B4],
        [B3, B4, B5],
    ])
    return B

def computeIntrinsicMatrixFrombCholesky(b: tuple):
    """
    Computes the intrinsic matrix from the vector b using
    Cholesky decomposition.

    Input:
        b -- vector made up of (B0, B1, B2, B3, B4, B5)^T

    Output:
        A -- intrinsic matrix

    Notes:
        Recall,

        B = (A^-1)^T * A^-1, where B = [B0 B1 B3]
                                       [B1 B2 B4]
                                       [B3 B4 B5]
        let L = (A^-1)^T
        then B = L * L^T
        L = Chol(B)

        and
        A = (L^T)^-1
    """
    B0, B1, B2, B3, B4, B5 = b
    # ensure B is positive semi-definite
    sign = +1
    if B0 < 0 or B2 < 0 or B5 < 0:
        sign = -1
    B = sign * np.array([
        [B0, B1, B3],
        [B1, B2, B4],
        [B3, B4, B5],
    ])
    L = np.linalg.cholesky(B)
    A = np.linalg.inv(L.T)
    A /= A[2,2]
    return A

def computeExtrinsics(Hs: list, A: np.ndarray):
    """
    Input:
        Hs -- list of homographies
        A -- intrinsic matrix

    Output:
        worldToCameraTransforms -- list of transform matrices from world to camera
    """
    Ainv = np.linalg.inv(A)
    worldToCameraTransforms = []
    for H in Hs:
        h0 = H[:,0]
        h1 = H[:,1]
        h2 = H[:,2]

        λ = 1 / np.linalg.norm(Ainv @ h0)

        r0 = λ * Ainv @ h0
        r1 = λ * Ainv @ h1
        r2 = np.cross(r0, r1)

        t = λ * Ainv @ h2

        # Q is not in SO(3)
        Q = np.hstack((mu.col(r0), mu.col(r1), mu.col(r2)))

        # R is in SO(3)
        R = approximateRotationMatrix(Q)
        transformWorldToCamera = mu.poseFromRT(R, t)
        worldToCameraTransforms.append(transformWorldToCamera)
    return worldToCameraTransforms

def approximateRotationMatrix(Q: np.ndarray):
    """
    Input:
        Q -- a 3x3 matrix which is close to a rotation matrix

    Output:
        R -- a 3x3 rotation matrix which is in SO(3)

    Method from Zhang paper, Appendix C

    Notes:
        minimize R in frobenius_norm(R - Q) subject to R^T * R = I

        frobenius_norm(R - Q) = trace((R - Q)^T * (R - Q))
                              = 3 + trace(Q^T * Q) - 2*trace(R^T * Q)

        so equivalently, maximize trace(R^T * Q)

        let
            U, S, V^T = svd(Q)

        we define
            Z = V^T * R^T * U
        ==>
            trace(R^T * Q)
            trace(R^T * U * S * V^T)
            trace(V^T * R^T * U * S)
            trace(Z * S)
    """
    U, S, V_T = np.linalg.svd(Q)
    R = U @ V_T
    return R


import numpy as np

def matrix_omp(A, Y, tol=None, nnz=None):
    """
    Orthogonal Matching Pursuit algorithm when the signal is a matrix.

    Parameters
    ----------
    A : np.ndarray with 2 dims
        Input dictionary. Columns are assumed to have unit norm.
    Y : np.array with 2 dims
        Input targets
    nnz : int
        Targeted number of non-zero elements.
    tol : float
        Targeted precision.
    """

    X = np.zeros((A.shape[1], Y.shape[1]))

    for k in range(Y.shape[1]):
        X_k, idx_k = omp(A, Y[:,k], tol, nnz)
        X[idx_k,k] = X_k

    return X

def omp(A, y, tol=None, nnz=None):
    """
    Orthogonal Matching Pursuit algorithm.

    Parameters
    ----------
    A : np.ndarray with 2 dims
        Input dictionary. Columns are assumed to have unit norm.
    y : np.array with 1 dim
        Input targets
    nnz : int
        Targeted number of non-zero elements.
    tol : float
        Targeted precision.
    """

    m, n = A.shape

    if nnz is None and tol is None:
        raise ValueError("Either nnz or tol must be specified.")
    if nnz is not None and tol is not None:
        tol = None
    if nnz is None:
        nnz = n
    elif nnz > n:
        raise ValueError("Parameter nnz exceed A.shape[1].")
    if tol is None:
        tol = 0

    idx = []
    n_active = 0
    Aty = A.T @ y
    res = y

    while True:
        k = np.argmax(np.abs(A.T @ res))

        if k in idx:
            print("warning : OMP stopped because the same atom was selected twice.")
            break

        n_active += 1
        idx.append(k)
        x_idx = np.linalg.lstsq(A[:,idx], y, rcond=None)[0]
        res = y - A[:,idx] @ x_idx

        if np.linalg.norm(res) < tol or n_active >= nnz:
            break

    return x_idx, idx


# coding: utf-8

import pandas as pd
import numpy as np

def dsigmoid(x):
    return np.exp(x)/ (1.0 + np.exp(x))**2

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

class MLP:
    def __init__(self):
        self.W = [None]
        self.A = [None]
        self.Z = [None]
        self.b = [None]
        self.layers = 0
        self.i_nodes = 0

    def add_input_layer(self,n_nodes):
        self.A[0] = np.empty(n_nodes,dtype=object)
        self.i_nodes = n_nodes

    def add_layer(self,n_nodes):
        if(self.layers == 0 ):
            self.W.append(np.random.randn(self.i_nodes,n_nodes))
        else:
            self.W.append(np.random.randn(self.W[self.layers].shape[1],n_nodes))
        self.b.append(np.zeros((n_nodes, 1)))
        self.layers += 1

    def forward(self,X):

        self.A = [None]
        self.Z = [None]

        self.A[0] = X
        L = self.layers
        for l in range(1,L+1): # 1 to L
            self.Z.append(np.dot(self.W[l].T,self.A[l-1]) + self.b[l])  #Z[l] created
            self.A.append(sigmoid(self.Z[l]))                   #A[l] created

    def back_prop(self,X,Y):
         self.A[0] = X
         L = self.layers
         m = X.shape[1]
         self.dZ = [None for _ in range(L+1)]
         self.dW = [None for _ in range(L+1)]
         self.db = [None for _ in range(L+1)]

         self.dZ[L] = np.multiply((self.A[L] - Y),dsigmoid(self.Z[L]))
         self.dW[L] = (1/m) * np.dot(self.A[L-1],self.dZ[L].T)
         self.db[L] = (1/m) * np.sum(self.dZ[L], axis=1, keepdims=True)

         for l in range(L-1,0,-1):
             self.dZ[l] = np.multiply(np.dot(self.W[l+1],self.dZ[l+1]),
                                                    dsigmoid(self.Z[l]))
             self.dW[l] = (1/m) * np.dot(self.A[l-1],self.dZ[l].T)
             self.db[l] = (1/m) * np.sum(self.dZ[l], axis=1, keepdims=True)

    def train(self, X, Y, epochs=100000, learning_rate=1.2):
        """ Complete process of learning, alternates forward pass,
            backward pass and parameters update """
        self.losses = []
        m = X.shape[0]
        for e in range(epochs):
            L = self.layers
            self.forward(X.T)
            loss = np.sum((Y.T - self.A[L])**2)/ m
            self.back_prop(X.T, Y.T)
            self.losses.append(loss)

            for l in range(1,L+1):
                self.W[l] -= learning_rate * self.dW[l]
                self.b[l] -= learning_rate * self.db[l]
            if e % 1000 == 0:
                print("Loss ",  e+1, " = ", loss)

    def predict(self,X):
        self.forward(X.T)
        return self.A[self.layers].T


from sympy import cancel, expand, symbols

def span(m, P1, n, P2):
    return m*P1[0] + n*P2[0], m*P1[1] + n*P2[1], m*P1[2] + n*P2[2]

def cross_ratio(A, B, C, D):
    a, b, c, d = A[0]/A[2], B[0]/B[2], C[0]/C[2], D[0]/D[2]
    return cancel((a - c)*(b - d)/(a - d)/(b - c))

def dep_coeff_index(A, B, C, i, j):
    return expand(B[i]*C[j] - B[j]*C[i]), expand(A[j]*C[i] - A[i]*C[j])

def dep_coeff(A, B, C):
    # return (m, n) such that kC=mA+nB
    m, n = dep_coeff_index(A, B, C, 0, 1)
    if m != 0 and n != 0:
        return m, n
    m, n = dep_coeff_index(A, B, C, 1, 2)
    if m != 0 and n != 0:
        return m, n
    m, n = dep_coeff_index(A, B, C, 2, 0)
    if m != 0 and n != 0:
        return m, n
    return None

def cross_ratio_2(A, B, C, D):
    # C=pA+qB, D=rA+sB, (A,B;C,D)=qr/ps
    p, q = dep_coeff(A, B, C)
    r, s = dep_coeff(A, B, D)
    return cancel(q*r/p/s)

def main():
    a, b, c, d, e, f, g, h, j, k, m, n = symbols('a, b, c, d, e, f, g, h, j, k, m, n')
    A, B = (a, 0, b), (c, 0, d)
    C, D = span(1, A, m, B), span(1, A, n, B)
    print('C=A+mB, D=A+nB:')
    print('(A,B;C,D) =', cross_ratio(A, B, C, D))
    print('(A,B;C,D) =', cross_ratio_2(A, B, C, D))
    C, D, E, F = span(e, A, f, B), span(g, A, h, B), span(j, A, k, B), span(m, A, n, B)
    print('C=eA+fB, D=gA+hB, E=jA+kB, F=mA+nB:')
    print('(C,D;E,F) =', cross_ratio(C, D, E, F))
    print('(C,D;E,F) =', cross_ratio_2(C, D, E, F))

if __name__ == '__main__':
    main()


"""

 kmeans.py  (author: Anson Wong / git: ankonzoid)

"""
import os
import numpy as np
import pandas as pd

class KMeans():

    def __init__(self, k=4):
        self.k = k

    def fit(self, X):
        N, d = X.shape
        self.cluster_centers = X[np.random.choice(N, self.k, replace=False)]
        converge = False
        y = -1 * np.zeros(len(X))
        while not converge:
            # Create class dictionary
            class_dict = {}
            for k in range(self.k):
                class_dict[k] = []

            # Assign class labels to data based on closest distance to cluster centers
            for i in range(N):
                distances = [np.linalg.norm(X[i] - self.cluster_centers[k]) for k in range(self.k)]
                c = np.argmin(distances)
                y[i] = c
                class_dict[c].append(i)

            # Find center of mass
            cluster_centers_new = []
            for k in range(self.k):
                cluster_center_new = np.mean(X[class_dict[k]], axis=0)
                cluster_centers_new.append(cluster_center_new)
            cluster_centers_new = np.array(cluster_centers_new)

            # Compute average deviation of cluster centers
            d_deviation_avg = 0
            for k in range(self.k):
                d_deviation_avg += np.linalg.norm(cluster_centers_new[k] - self.cluster_centers[k])
            d_deviation_avg /= self.k

            # Set new cluster center
            self.cluster_centers = cluster_centers_new

            # Announce convergence if d_deviation_avg is small enough
            threshold = 1E-10
            if d_deviation_avg < threshold:
                converge = True

    def predict(self, X):
        # Assign class labels to data based on closest distance to cluster centers
        N, d = X.shape
        y_pred = -1 * np.zeros(len(X))
        for i in range(N):
            distances = [np.linalg.norm(X[i] - self.cluster_centers[k]) for k in range(self.k)]
            c = np.argmin(distances)
            y_pred[i] = c
        return y_pred

# Main driver
if __name__ == "__main__":

    def load_csv_data(data_csv_filename, mode="clf", verbose=False):
        if verbose:
            print("Loading data from '{}' (mode={})...".format(data_csv_filename, mode))
        df = pd.read_csv(data_csv_filename)  # dataframe
        df_header = df.columns.values  # header
        header = list(df_header)
        N, d = len(df), len(df_header) - 1
        X = np.array(df.drop(['y'], axis=1))  # extract X by dropping y column
        y_raw = np.array(df['y'])  # extract y

        y_classes_raw = sorted(list(set(y_raw)))
        y_classes_dict = {}
        for i, y_class_i in enumerate(y_classes_raw):
            y_classes_dict[y_class_i] = i

        y = np.array([y_classes_dict[y_i] for y_i in y_raw])
        y_classes = list(set(y))
        assert X.shape == (N, d)  # check X.shape
        assert y.shape == (N,)  # check y.shape
        if mode == "clf":
            assert y.dtype in ['int64']  # check y are integers
        elif mode == "regr":
            assert y.dtype in ['int64', 'float64']  # check y are integers/floats
        else:
            exit("err: invalid mode given!")
        if verbose:
            print(" header={}\n X.shape={}\n y.shape={}\n len(y_classes)={}\n".format(header, X.shape, y.shape, len(y_classes)))
        return X, y, header

    data_csv_data_filename = os.path.join("data", "iris.data.txt")
    X, y, header = load_csv_data(data_csv_data_filename, mode="clf", verbose=True)
    model = KMeans(k=4)
    model.fit(X)
    y_pred = model.predict(X)
    print("y_pred", y_pred)


import numpy as np

import math
class Gaussian:

    def __init__(self, mu=0, sigma=1):

        self.mean = mu
        self.stdev = sigma
        self.data = []

    def calculate_mean(self):

        self.mean = np.mean(self.data)
        return self.mean

    def calculate_stdev(self, sample=True):

        x_mean = self.calculate_mean()

        mean_item_squared = []
        for i in range(len(self.data)):
            mean_item = (self.data[i] - x_mean)**2
            mean_item_squared.append(mean_item)

        self.stdev = math.sqrt(np.sum(mean_item_squared) / len(self.data))
        sample_length = len(self.data)
        if sample:
            self.stdev = math.sqrt(np.sum(mean_item_squared) / (sample_length-1))
            return self.stdev

        return self.stdev

    def read_data_file(self, file_name, sample=True):

        with open(file_name) as file:
            data_list = []
            line = file.readline()
            while line:
                data_list.append(line)
                line = file.readline()
        file.close()
        self.data = data_list
        self.mean = self.calculate_mean()
        self.stdev = self.calculate_stdev(sample=True)

    def __add__(self, other):
        results = Gaussian()
        results.mean = self.mean + other.mean
        results.stdev = math.sqrt(self.stdev**2 + other.stdev**2)

        return results

    def __repr__(self):
        return f'mean is {self.mean}, stdev is {self.stdev}'

data = [9, 2, 5, 4, 12, 7]

gaussian = Gaussian()
gaussian.data = data
print(gaussian.calculate_mean())
print(gaussian.calculate_stdev(sample=True))
gaussian_one = Gaussian(5, 2)
gaussian_two = Gaussian(7, 3)
gaussian_sum = gaussian_one + gaussian_two
print(gaussian_sum)
print(gaussian_sum.stdev)
print(gaussian_sum.mean)


# # # *************** Numpy *****************

# NumPy is used for working with arrays
# NumPy is short for "Numerical Python"
# It also has functions for working in domain of linear algebra, fourier transform, and matrices

# In Python we have lists that serve the purpose of arrays, but they are slow to process.
# NumPy aims to provide an array object that is up to 50x faster than traditional Python lists
# NumPy arrays are stored at one continuous place in memory unlike lists, so processes can access and manipulate them very efficiently
# NumPy is a Python library and is written partially in Python, but most of the parts that require fast computation are written in C or C++

import numpy as np

a = np.array(42) # 0-D Array
b = np.array([1, 2, 3, 4, 5]) # 1-D Array
c = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]) # 2-D Array
d = np.array([[[1, 2, 3], [4, 5, 6]], [[1, 2, 3], [4, 5, 6]]]) # 3-D Array
print(type(b)) # <class 'numpy.ndarray'>

# data type
# i - integer
# b - boolean
# u - unsigned integer
# f - float
# c - complex float
# m - timedelta
# M - datetime
# O - object
# S - string
# U - unicode string
# V - fixed chunk of memory for other type ( void )

# numpy.bool_                     Boolean (True or False) stored as a byte
# numpy.byte                      Platform-defined
# numpy.ubyte                     Platform-defined
# numpy.short                     Platform-defined
# numpy.ushort                    Platform-defined
# numpy.intc                      Platform-defined
# numpy.uintc                     Platform-defined
# numpy.int_                      Platform-defined
# numpy.uint                      Platform-defined
# numpy.longlong                  Platform-defined
# numpy.ulonglong                 Platform-defined
# numpy.half / numpy.float16      Half precision float: sign bit, 5 bits exponent, 10 bits mantissa
# numpy.single                    Platform-defined single precision float: typically sign bit, 8 bits exponent, 23 bits mantissa
# numpy.double                    Platform-defined double precision float: typically sign bit, 11 bits exponent, 52 bits mantissa
# numpy.longdouble                Platform-defined extended-precision float
# numpy.csingle                   Complex number, represented by two single-precision floats (real and imaginary components)
# numpy.cdouble                   Complex number, represented by two double-precision floats (real and imaginary components)
# numpy.clongdouble               Complex number, represented by two extended-precision floats (real and imaginary components)

# numpy.int8      Byte (-128 to 127)
# numpy.int16     Integer (-32768 to 32767)
# numpy.int32     Integer (-2147483648 to 2147483647)
# numpy.int64     Integer (-9223372036854775808 to 9223372036854775807)
# numpy.uint8     Unsigned integer (0 to 255)
# numpy.uint16    Unsigned integer (0 to 65535)
# numpy.uint32    Unsigned integer (0 to 4294967295)
# numpy.uint64    Unsigned integer (0 to 18446744073709551615)
# numpy.intp      Integer used for indexing, typically the same as ssize_t
# numpy.uintp     Integer large enough to hold a pointer
# numpy.float32
# numpy.float64 / numpy.float_         (builtin python float)
# numpy.complex64
# numpy.complex128 / numpy.complex_    (builtin python complex)

c = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]], dtype=int) # integer
c = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]], dtype='i') # integer
c = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]], dtype='i4') # integer with 4 bytes
c = np.array([[1.5, 2, 3.5, 4, 5], [6, 0.4, 8, 9.1, 10]], dtype=np.float32) # folat with 32 bytes
c = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]], dtype=np.int16) # integer with 16 bytes (-32768 to 32767)

array1 = np.zeros((3, 4), dtype=int)   # array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])
array2 = np.ones((3, 4), dtype=int)    # array([[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]])
array3 = np.full((3, 4), 5, dtype=int) # array([[5, 5, 5, 5], [5, 5, 5, 5], [5, 5, 5, 5]])
array3 = np.eye(3) # array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])
array4 = np.arange(1, 6) # [1, 2, 3, 4, 5]
array5 = np.arange(12).reshape(3, 4) # array([[ 0,  1,  2,  3], [ 4,  5,  6,  7], [ 8,  9, 10, 11]])

array = np.array([[2, 5, 9], [5, 8, 3]]) # 2D array (metrics)
print('Array :', array)
print('Shape :', array.shape)
print('Dimension :', array.ndim)
print('Data Type :', array.dtype)

arr = np.array([1, 2, 3, 4], ndmin=5) # creating array of 5 Dimension

# ndarray supports indexing and negative indexing just like list
print(b[1])         # in list: lst[1]      1-D array
print(array[1, 2])  # in list: lst[1][2]   2-D array
print(array[0, -2]) # in list: lst[0][-2]  2-D array

# Array slicing
print(b[1:4]) # slicing 1-D array
print(b[1:4:2]) # slicing 1-D array with step
print(c[1, 1:4])   # slicing 2-D array (From the second element, slice elements from index 1 to index 3)
print(c[0:2, 2])   # slicing 2-D array (From both elements, return index 2)
print(c[0:2, 2:5]) # slicing 2-D array (From both elements, slice index 1 to index 4, returns a 2-D array)

# Copy vs View
array1 = np.array([[2, 5, 9], [5, 8, 3]], dtype=np.int8)

array2 = array1.copy()
array2[0, 1] = 10 # this will only change 'array2', not the 'array1'

array3 = array1.view()
array3[0, 1] = 10 # this will both change 'array1' and 'array3'

# Check copy / view
print('copy base : ', array2.base) # None
print('view base : ', array3.base) # <that array>

# Array Reshape
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])

newarr = arr.reshape(3, 4)     # reshaping to 2D array (3 * 4 = 12 = total elements)
newarr = arr.reshape(2, 3, 2)  # reshaping to 3D array (2 * 3 * 2 = 12 = total elements)
# will raise an error, if it can not be reshaped to the given shape

# Iterating
arr = np.array([[3, 2, 4], [5, 0, 1]])
for x in arr: # 2D array
  for y in x:
    print(y)

# Iterating n-D array using nditer()
arr = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
for x in np.nditer(arr):
  print(x)

# Enumerated Iteration Using ndenumerate()
for idx, x in np.ndenumerate(arr):
  print('Index :', idx, 'Value :', x)

# Joining arrays
arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])
arr = np.concatenate((arr1, arr2))  # [1 2 3 4 5 6]

arr3 = np.array([[1, 2], [3, 4]])
arr4 = np.array([[5, 6], [7, 8]])
arr = np.concatenate((arr3, arr4), axis=1) # along rows     | [[1 2 5 6], [3 4 7 8]]
arr = np.concatenate((arr3, arr4), axis=0) # along columns  | [[1 2], [3 4], [5 6], [7 8]]

# same as concatenation, the only difference is that stacking is done along a new axis
arr = np.stack((arr3, arr4), axis=1)  # [[1 4], [2 5], [3 6]]
arr = np.stack((arr3, arr4), axis=0)  # [[1 2 3], [4 5 6]]
# Stacking Along Rows, Columns and Height (depth)
arr = np.hstack((arr3, arr4))  # [1 2 3 4 5 6]
arr = np.vstack((arr3, arr4))  # [[1 2 3], [4 5 6]]
arr = np.dstack((arr1, arr2))  # [[[1 4], [2 5], [3 6]]]

# Spliting arrays (reverse operation of Joining)
arr = np.array([1, 2, 3, 4, 5, 6])
newarr = np.array_split(arr, 3)  # [array([1, 2]), array([3, 4]), array([5, 6])]
# If the array has less elements than required, it will adjust from the end accordingly
newarr = np.array_split(arr, 4)  # [array([1, 2]), array([3, 4]), array([5]), array([6])]

# Split the 2-D array into three 2-D arrays
arr = np.array([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12]])
newarr = np.array_split(arr, 3)  # [array([[1, 2], [3, 4]]), array([[5, 6], [7, 8]]), array([[ 9, 10], [11, 12]])]

# Spliting along rows
arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12], [13, 14, 15], [16, 17, 18]])
newarr = np.array_split(arr, 3, axis=1) # [array([[ 1], [ 4], [ 7], [10], [13], [16]]), array([[ 2], [ 5], [ 8], [11], [14], [17]]), array([[ 3], [ 6], [ 9], [12], [15], [18]])]
newarr = np.hsplit(arr, 3)              # same
# Spliting along culmns
newarr = np.array_split(arr, 3, axis=0) # [array([[1, 2, 3], [4, 5, 6]]), array([[ 7,  8,  9], [10, 11, 12]]), array([[13, 14, 15], [16, 17, 18]])]
newarr = np.vsplit(arr, 3)              # same
# Spliting along Height (depth)
# newarr = np.dsplit(arr, 3) # height/depth split only works for 3 or higher dimension

# Searching Arrays
arr = np.array([1, 2, 3, 4, 5, 4, 4])
x = np.where(arr == 4)    # Find the indexes where the value is 4
x = np.where(arr%2 == 0)  # Find the indexes where the values are even

# Search Sorted (returns the index where the specified value would be inserted to maintain the search order)
arr = np.array([6, 7, 8, 9]) # a sorted arrary
x = np.searchsorted(arr, 7) # Find the indexes where the value 7 should be inserted : 2
x = np.searchsorted(arr, 7, side='right') # Find the indexes where the value 7 should be inserted, starting from the right : 2
x = np.searchsorted(arr, [4, 2, 6]) # Find the indexes where the values 2, 4, and 6 should be inserted : [2, 1, 3]

# Sorting
arr = np.array([3, 2, 0, 1])
newarr = np.sort(arr) # return the sorted array : [0, 1, 2, 3]

arr = np.array([[3, 2, 4], [5, 0, 1]])
print(np.sort(arr))  # [[2, 3, 4], [0, 1, 5]]

# Filter
arr = np.array([41, 42, 43, 44])
x = [True, False, True, False]
newarr = arr[x] # [41, 43]

filter_arr = arr > 42 # [False, False, True, True]
newarr = arr[filter_arr] # [43, 44]
# in one line : newarr = arr[arr > 42]

filter_arr = arr % 2 == 0 # [False, True, False, True]
newarr = arr[filter_arr] # [42, 44]

arr = np.array([[3, 2, 4], [5, 0, 1]])
newarr = arr[arr % 3 != 1] # [3 2 5 0]

# Reversing / Fliping
arr = np.array([41, 43, 42, 44])
arr2 = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

arr_flipped = np.flip(arr)          # array([44, 42, 43, 41])
arr_flipped = np.flip(arr2)         # array([[9, 8, 7], [6, 5, 4], [3, 2, 1]])  | Flip in the both axis
arr_flipped = np.flip(arr2, axis=0) # array([[7, 8, 9], [4, 5, 6], [1, 2, 3]])  | Flip the rows | Alt.: flipud()
arr_flipped = np.flip(arr2, axis=1) # array([[3, 2, 1], [6, 5, 4], [9, 8, 7]])  | Flip only in the row | Alt.: fliplr ()

# Random
from numpy import random
arr = random.randint(100, size=(5)) # Generate a 1-D array containing 5 random integers from 0 to 100
arr = random.randint(100, size=(3, 5)) # Generate a 2-D array with 3 rows, each row containing 5 random integers from 0 to 100
arr = random.rand(3, 5) # Generate a 2-D array with 3 rows, each row containing 5 random numbers
arr = random.choice([3, 5, 7, 9], size=(3, 5))

# Generate a 1-D array containing 50 values, where each value has to be 3, 5, 7 or 9.
# The probability for the value to be 3 is set to be 0.1
# The probability for the value to be 5 is set to be 0.3
# The probability for the value to be 7 is set to be 0.6
# The probability for the value to be 9 is set to be 0
# The sum of all probability numbers should be 1
arr = random.choice([3, 5, 7, 9], p=[0.1, 0.3, 0.6, 0.0], size=(50))

arr = random.choice([3, 5, 7, 9], p=[0.1, 0.3, 0.6, 0.0], size=(3, 5)) # 2D array

arr = np.array([1, 2, 3, 4, 5])
random.shuffle(arr) # shuffle changes orginial array
newarr = random.permutation(arr)

x = np.random.uniform(0.0, 5.0, 250) # low : 0.0  &  high : 5.0  &  size : 250
x = np.random.normal(5.0, 1.0, 1000) # mean : 5.0  &  std: 1.0  &  size : 1000

# Numpy ufunc
# ufuncs stands for "Universal Functions" and they are NumPy functions that operates on the ndarray object
# ufuncs are used to implement vectorization in NumPy which is way faster than iterating over elements

# Create Your Own ufunc
# The frompyfunc() method takes the following arguments:
#   function - the name of the function.
#   inputs   - the number of input arguments (arrays).
#   outputs  - the number of output arrays.

def mycalc(x, y):
  return (x + y) / 2

mycalc = np.frompyfunc(mycalc, 2, 1) # here, 2 is number of input arrays, 1 is number of output arrays

print(mycalc([1, 2, 3, 4], [5, 6, 7, 8])) # [3.0 4.0 5.0 6.0]

print(type(np.add)) # <class 'numpy.ufunc'>
print(type(mycalc))  # <class 'numpy.ufunc'>

# ufunc Simple Arithmetic
arr1 = np.array([10, 11, 12, 13, 14, 15])
arr2 = np.array([20, 21, 22, 23, 24, 25])

newarr = np.add(arr1, arr2) # [30 32 34 36 38 40]
newarr = arr1 + arr2
newarr = np.subtract(arr1, arr2) # [-10 -10 -10 -10 -10 -10]
newarr = arr1 - arr2
newarr = np.multiply(arr1, arr2) # [200 231 264 299 336 375]
newarr = arr1 * arr2
newarr = np.divide(arr1, arr2) # [0.5 0.52380952 0.54545455 0.56521739 0.58333333 0.6]
newarr = arr1 / arr2
newarr = np.power(arr1, arr2) # [ 7766279631452241920  3105570700629903195  5729018530666381312 -4649523274362944347 -1849127232522420224  1824414961309619599]
newarr = arr1 ** arr2
newarr = np.mod(arr1, arr2) # [10 11 12 13 14 15]
newarr = arr1 % arr2
newarr = np.divmod(arr1, arr2) # (with remainders) (array([0, 0, 0, 0, 0, 0]), array([10, 11, 12, 13, 14, 15]))
newarr = arr1 // arr2 # [0, 0, 0, 0, 0, 0]

newarr = np.absolute([-1, -2, 1, 2, 3, -4]) # [1, 2, 1, 2, 3, 4]

# just like that we can give any other value, like:
newarr = arr1 + 2 # [12 13 14 15 16 17]
newarr = arr1 % 3 # [1 2 0 1 2 0]

# ufunc mathematical operations
arr = np.array([-3.1666, 3.6667])

# Remove the decimals, and return the float number closest to zero
newarr = np.trunc(arr)      # [-3.  3.]
newarr = np.fix(arr)        # [-3.  3.]
# round float
newarr = np.around(arr)  # [-3.  4.]
# round float to 2 decimal points
newarr = np.around(arr, 2)  # [-3.17  3.67]
# The floor() function rounds off decimal to nearest lower integer
newarr = np.floor(arr)      # [-4.  3.]
# The ceil() function rounds off decimal to nearest upper integer
newarr = np.ceil(arr)       # [-3.  4.]

arr = np.arange(1, 10)

newarr = np.log2(arr)  # base 2
newarr = np.log10(arr) # base 10
newarr = np.log(arr)   # base e

from math import log
nplog = np.frompyfunc(log, 2, 1)
# np do not provide func for log with any base, so we create our own
newarr = nplog(arr, 4)   # any base

# like these, there is:
# np.sin(), np.cos(), np.tan(), np.arcsin(), np.arccos(), np.arctan()
# np.sinh(), np.cosh(), np.tanh(), np.arcsinh(), np.arccosh(), np.arctanh()
# np.deg2rad(), np.rad2deg(), np.hypot()

arr = np.array([1, 2, 3, 4])
arr1 = np.array([1, 2, 3])
arr2 = np.array([1, 2, 3])

# Summations
newarr = np.sum([arr1, arr2]) # 12
newarr = np.sum([arr1, arr2], axis=1) # [6 6]

# Cummulative sum : partially adding the elements in array.
# E.g. The partial sum of [1, 2, 3, 4] would be [1, 1+2, 1+2+3, 1+2+3+4] = [1, 3, 6, 10]
newarr = np.cumsum(arr) # [1, 3, 6, 10]

# Products
newarr = np.prod(arr) # 24 because 1*2*3*4 = 24
newarr = np.prod([arr1, arr2], axis=1) # [6 6]

# Just like Cummulative sum, there is Cummulative Product
# E.g. The partial sum of [1, 2, 3, 4] would be [1, 1*2, 1*2*3, 1*2*3*4] = [1, 2, 6, 24]
newarr = np.cumprod(arr) # [1, 2, 6, 24]

# Discrete Difference
arr = np.array([10, 15, 25, 5])
newarr = np.diff(arr) # [5 10 -20] because 15-10=5, 25-15=10, and 5-25=-20

# LCD & GCD
lcd = np.lcm.reduce(arr) # 150
gcd = np.gcd.reduce(arr) # 5

# Compare two arrays and returns a new array containing the element-wise minima
np.minimum([2, 3, 4], [1, 5, 2]) # array([1, 3, 2])
# Compare two arrays and returns a new array containing the element-wise maxima
np.maximum([2, 3, 4], [1, 5, 2]) # array([2, 5, 4])

min_value = arr.min()
max_value = arr.max()
print(arr.T)              # transpose

# Statistics
mean = np.mean(arr)      # avarage value
median = np.median(arr)  # the midest value

std = np.std(arr)        # Standard Deviation
var = np.var(arr)        # Variance

ages = [5,31,43,48,50,41,7,11,15,39,80,82,32,2,8,6,25,36,27,61,31]
x = np.percentile(ages, 75)
# What is the 75 percentile? The answer is 43, meaning that 75% of the people are 43 or younger.

# Sets in Numpy (A set in mathematics is a collection of unique elements)
arr = np.array([4, 6, 8, 4, 6])
newarr = np.unique(arr)

arr1 = np.array([1, 2, 3, 4])
arr2 = np.array([3, 4, 5, 6])

newarr = np.union1d(arr1, arr2)                         # Union
newarr = np.intersect1d(arr1, arr2, assume_unique=True) # Intersection
newarr = np.setdiff1d(arr1, arr2, assume_unique=True)   # Difference
newarr = np.setxor1d(arr1, arr2, assume_unique=True)    # Symmetric Difference

# assume_unique : which if set to True can speed up computation. It should always be set to True when dealing with sets


"""
   :copyright: (c) 2020 by Gustavo Landfried
   :license: BSD, see LICENSE for more details.
"""
#import os
#import math
import numpy as np
from numpy.random import normal as rnorm
from matplotlib import pyplot as plt

#__all__ = ["dW","wiener"]

"""
---//---//---//---//---//---//---//---//---//---//---//---//---//---//---//---

                      Geometrical Browninan Motion.

Below you will find the functions to create the geometrical Browninan Motion

---//---//---//---//---//---//---//---//---//---//---//---//---//---//---//---
"""

"""
First of all we need to define the Norbert Wiener process: A real valued
continuous time stochastic process of the one-dimensional Brownian motion.
    - $W_0=0$
    - $W_t$ has Gaussian independent increments centered at $W_{t-1}$
    - $W$ has continuous paths: $W_{t}$ is continuous in $t$.
"""

def dW(pos=0,step=1):
    """
    The Gaussian independent increments of Norbert Wiener process.
    """
    return rnorm(pos,step,1)[0]

def wiener(iterations,step_length):
    """
    Norbert Wiener process:
        A real valued continuous time stochastic process
        of the one-dimensional Brownian motion.
    """
    res = [0]
    for i in range(iterations):
        pos = res[-1]
        res.append(dW(pos,step_length))
    return res

def show_walks(n,iteratons=100,step_length=1):
    """
    Examples
    ---------
    show_walks(10)
    show_walks(10,1000,0.01) # See changes on scale
    """
    for i in range(n):
        plt.plot(wiener(iteratons,step_length))

"""
Now, we need to define a multiplicative process. We first will define the
constant multiplicative process, and then we will perturbed.
"""

def multiplicative_process(n,rate,dt):
    """
    A constant growth rate process

    Parameters
    ----------
    n : int
        Number of time steps
    rate : float
        Constant growth rate
    dt : TYPE
        Delta time between time steps
    """
    wealth = [1]
    time = [0]
    for i in range(n):
        wealth.append(wealth[-1]*rate)
        time.append(time[-1]+dt)
    return time, wealth

def show_mult(n=120,rate=1.01,dt=1/12):
    """
    show_mult(100,1.04,dt=1)
    show_mult(12*12,1.01,dt=1/12)
    """
    x, y = multiplicative_process(n,rate,dt)
    plt.plot(x,np.log10(y))

def gr_mult(rate,dt):
    """
    Constant growth rate in a multiplicative process

    Examples
    --------
    gr_mult(1.125,1)
    gr_mult(1.01,1/12)
    """
    return np.log(rate)/dt

"""
To perturb the process in a consistent way, we remind ourselves
that what's constant about the process in the absence of noise is the
growth rate
"""

def perturbed_payment(rate=1.01,dt=1/12,sigma=0.1):
    """
    perturbed_payment(1.125,1,1)
    """
    gamma = np.log(rate)/dt
    dv = gamma*dt + sigma*dW(0,dt)
    return max(dv,-0.9999)

def walk_perturbed_payment(n,rate,dt,sigma=100,log=False):
    """
    walk_perturbed_payment(100,1.01,1/12,3)
    """
    wealth = [1]
    for _ in range(n):
        dv = perturbed_payment(rate,dt,sigma)
        wealth.append(wealth[-1]+wealth[-1]*dv)
    return wealth

def multiplicative_perturbed_history(n=100,iterations=1000,rate=1.005,dt=1/12,sigma=2):
    res = []
    for i in range(n):
        res.append(walk_perturbed_payment(iterations,rate,dt,sigma))
    return res

def show_walk_perturbed_payment(n=10,iterations=1000,rate=1.005,dt=1/12,sigma=2):
    """
    show_walk_perturbed_payment(n=10,sigma=2)
    plt.show()
    show_walk_perturbed_payment(n=1,sigma=0)
    """
    for i in range(n):
        plt.plot(np.log10(walk_perturbed_payment(iterations,rate,dt,sigma)) )

def gross_domestic_product(wealths):
    return np.sum(wealths)

def gross_domestic_product_per_capita(wealths):
    return gross_domestic_product(wealths)/len(wealths)

def democratic_domestic_product(wealths):
    return np.prod(wealths)

def democratic_domestic_product_per_capita(wealths):
    return democratic_domestic_product(wealths)**(1/len(wealths))

def growth(state_t, state_0,dt=1):
    return (1/dt) * np.log(state_t/state_0)

def second_theil_index(gdp,ddp):
    return np.log(gdp) - np.log(ddp)

def gini(wealths):
    ys = np.array(sorted(wealths))
    denom = 2* sum([ (i+1)*ys[i] for i in range(len(ys))])
    numer = len(ys)* sum([ ys[i] for i in range(len(ys))])
    return denom/numer - (len(ys)+1)/len(ys)

"""
---//---//---//---//---//---//---//---//---//---//---//---//---//---//---//---

                    Other functions (some used in a post)

---//---//---//---//---//---//---//---//---//---//---//---//---//---//---//---
"""

def change_in_utility(w):
    """
    w = np.array(list(map(lambda x: 10.0**x,np.arange(-10,10))) )
    change_in_utility(w)
    """
    return 1/w

def utility(w):
    return np.log(w)

def show_utility(w):
    """
    w = np.arange(-10,10,step=0.1)
    show_utility(w)
    np.log(10**-10)
    """
    plt.plot(w,(utility(w)))
    plt.xticks(fontsize=12) # rotation=90
    plt.yticks(fontsize=12) # rotation=90
    plt.ylabel("Utility", fontsize=16 )
    plt.xlabel("Wealth", fontsize=16 )

"""
The Utility Theory considers that it is preferable to participate in this game
when the expected value is positive. Then, the Utility Theory systematically
observed how people acted contrary to what was considered optimal. Paired with
a firm belief in its models, this has led to a narrative of human irrationality
in large parts of economics.

But if we analyze what really

This is because we are facing a multiplicative process.
When the asymmetry between real physical effects of gains and losses is large, people quite reasonably 'pay to avoid losses' (e.g. buy insurance).
When it is not large, people don't unreasonably mind losses, and won't pay to avoid them.

"""

def simple_gamble(size):
    r = np.random.random()
    if r <= 0.5:
        res = 1.5*size
    else:
        res = 0.6*size
    return res

def walk_simple_gamble(iteratons):
    res = [1]
    for i in range(iteratons):
        res.append(simple_gamble(res[-1]))
    return res

def incest_rule(communities_size,exogamy=0.05):
    res = []
    migration_per_community = exogamy*sum(communities_size)/len(communities_size)
    for c in range(len(communities_size)):
        res.append(communities_size[c]*(1-exogamy) + migration_per_community)
    return res

def init_communities(n_communities):
    communities = []
    for i in range(n_communities):
        communities.append(1.0)
    return communities

def walk_incest(iteratons,n_communities,incest=1):
    communities = init_communities(n_communities)
    history = []
    history.append(communities)
    for i in range(iteratons):
        history.append(list(map(lambda x: simple_gamble(x), incest_rule(history[-1],incest) ) ))
    return history

def incest_rule_free_rider(communities_size,free_rider,exogamy=0.05):
    res = []
    n_free_riders = len(free_rider)
    size_not_free_rider = sum(map(lambda i: 0 if i in free_rider else communities_size[i], range(len(communities_size))))
    migration_per_community = exogamy*size_not_free_rider/len(communities_size)
    for c in range(len(communities_size)):#c=0
        res.append( communities_size[c]*(1-exogamy*int(not c in free_rider)) + migration_per_community)
    return res

def walk_incest_free_rider(iteratons,n_communities,n_free_riders=1,incest=1):
    communities = init_communities(n_communities)
    free_riders = list(range(n_free_riders))
    history = []
    history.append(communities)
    for i in range(iteratons):
        history.append(list(map(lambda x: simple_gamble(x), incest_rule_free_rider(history[-1],free_riders,incest) ) ))
    return history

"""
Share resource, but play you one game.
"""

def walk_sharing(iteratons=1000,n_communities=150):
    communities = init_communities(n_communities)
    history = []
    history.append(communities)
    for i in range(iteratons):
        proportion = list(map(lambda x: x/n_communities, history[-1] ))
        shared = sum(history[-1])
        shared = simple_gamble(shared) # El problema es jugar en grupo. Ley de los grandes n\'umeros.
        history.append(list(map(lambda x: shared*x, proportion)))
    return history