from typing import Dict, Optional, Any
import numpy as np
import scipy
import pandas as pd
import threading

def black_scholes(S, K, T, rf: float, iv, option_type) -> pd.DataFrame:
    """
    Black Scholes modeling function.

        * https://www.newyorkfed.org/medialibrary/media/research/staff_reports/sr677.pdf
        * https://github.com/hashABCD/opstrat/blob/main/opstrat/blackscholes.py
        * https://www.smileofthales.com/computation/options-greeks-python/
        * https://github.com/vpatel576/option_probabilites
        * https://github.com/skp1999/McMillan-s-Profit-Probability-Calculator/blob/main/POP_Calculation.py
        * https://option-price.com/index.php

    Option Value: Theoretical premium value.
    Delta : Measures Impact of a Change in the Price of Underlying
    Gamma: Measures the Rate of Change of Delta
    Theta: Measures Impact of a Change in Time Remaining
    Vega: Measures Impact of a Change in Volatility
    Rho: Measures the impact of changes in Interest rates

    :param S: Underlying Asset or Stock Price ($).
    :param K: Strike or Excercise Price ($).
    :param T: Expiry time of the option (days).
    :param rf: Risk-free rate (decimal number range 0-1).
    :param iv: Volatility (decimal).
    :param option_type: Calls or Puts option type.
    :return: Dataframe 'option_value', 'intrinsic_value', and 'time_value'.
     Greeks has delta, gamma, theta, rho.
    """
    # Check inputs.
    is_type = np.isin(option_type, ['calls', 'puts', 'call', 'put', 'c', 'p'])
    assert np.all(is_type) == 1, "Enter Calls or Puts options only."

    t = np.maximum(T / 365, 0.00001)  # Avoid infinite when T = 0.
    iv = np.maximum(iv, 0.00001)  # Avoid infinite when iv = 0.

    n1 = np.log(S / K)
    n2 = (rf + iv ** 2 / 2) * t
    d = iv * np.sqrt(t)

    d1 = (n1 + n2) / d
    d2 = d1 - d

    f = np.where(np.isin(option_type, ['calls', 'call', 'c']), 1, -1)

    N_d1 = scipy.stats.norm.cdf(f * d1)
    N_d2 = scipy.stats.norm.cdf(f * d2)

    A = S * N_d1
    B = K * N_d2 * np.exp(-rf * t)

    # Option pricing.
    val = f * (A - B)
    val_int = np.maximum(0.0, f * (S - K))
    val_time = val - val_int

    # Greeks.
    delta = f * N_d1
    gamma = np.exp((-d1 ** 2) / 2) / (S * iv * np.sqrt(2 * np.pi * t))
    theta = (-S * iv * np.exp(-d1 ** 2 / 2) / np.sqrt(8 * np.pi * t)
             - f * (N_d2 * rf * K * np.exp(-rf * t))) / 365
    vega = ((S * np.sqrt(t) * np.exp((-d1 ** 2) / 2))
            / (np.sqrt(2 * np.pi) * 100))
    rho = f * t * K * N_d2 * np.exp(-rf * t) / 100

    # Returns Dataframe.
    return pd.DataFrame({
        'option_value_bs': val,
        'intrinsic_value': val_int,
        'time_value': val_time,
        'delta': delta,
        'gamma': gamma,
        'theta': theta,
        'vega': vega,
        'rho': rho
    })

def monte_carlo(
        key: str,
        S: float,
        K: float,
        T: int,
        rf: float,
        iv: float,
        option_type: str,
        n: int = 200,
        rng: Any = None) -> Dict[str, Optional[float]]:
    """
    Monte Carlo modeling function.

    Monte Carlo allows us to simulate seemingly random events, and assess
    risks (among other results, of course). It has been used to assess the
    risk of a given trading strategy.

        * https://python.plainenglish.io/monte-carlo-options-pricing-in-two-lines-of-python-cf3a39407010
        * https://www.youtube.com/watch?v=sS7GtIFFr_Y
        * https://pythonforfinance.net/2016/11/28/monte-carlo-simulation-in-python/
        * https://aaaquants.com/2017/09/01/monte-carlo-options-pricing-in-two-lines-of-python/#page-content

    This function is not fully vectorized. It needs to be in a loop, with
    each row passed for processing. All results are summarized by a Numpy func.

    Usage:
    ::
            vector_profit_probability = np.vectorize(monte_carlo)
            pop = vector_profit_probability(
                S=curr_price,
                K=opt['strike'].to_numpy(),
                T=opt['dte'].to_numpy(),
                rf=ten_yr,
                iv=opt['volatility'].to_numpy(),
                option_type=opt['option_type'].to_numpy(),
                rng=rng,
                n=1000
            )

    :param key: Key per result. Useful for future concatenation.
    :param S: Underlying Asset or Stock Price ($).
    :param K: Strike or Excercise Price ($).
    :param T: Expiry time of the option (days).
    :param rf: Risk-free rate (decimal number range 0-1).
    :param iv: Volatility (decimal).
    :param option_type: Calls or Puts option type.
    :param n: Number of Monte Carlo iterantions. min 100,000 recommended.
    :param rng: Random range generator, used when in loops.
    :return: Dictionary with keys: value, greeks.
     Value has 'option_value', 'intrinsic_value', and 'time_value'.
     Greeks jas delta, gamma, theta, rho.
    """
    # Check inputs.
    assert option_type in ['calls', 'puts', 'call', 'put', 'c', 'p'], \
        "Enter Calls or Puts options only."

    # np.random.seed(25)  # Use for consistent testing.

    T = T if T != 0 else 1
    D = np.exp(-rf * (T / 252))

    # Randomized array of number of days x simulations, based of current price.
    # P = np.cumprod(1 + np.random.randn(n, T) * iv / np.sqrt(252), axis=1) * S
    # Generating random range is expensive, so doing it once.
    rng = np.random.Generator(np.random.PCG64()) if rng is None else rng
    rnd = rng.standard_normal((n, T), dtype=np.float32)
    P = np.cumprod(1 + rnd * iv / np.sqrt(252), axis=1) * S

    # Series on last day of simulation with premium difference.
    p_last = P[:, -1] - K * D

    # If calls, take only positive results. If puts, take negatives.
    if option_type in ['calls', 'call', 'c']:
        arr = np.where(p_last > 0, p_last, 0)
    else:
        arr = -np.where(p_last < 0, p_last, 0)

    # Take the average values of all the iterations on the last day.
    val = np.mean(arr)

    # Probability of Profit.
    pop_ITM = round(np.count_nonzero(arr) / p_last.size, 2)

    # Probability of Making 50% Profit.
    profit_req = 0.50
    if option_type in ['calls', 'call', 'c']:
        arr = np.where(p_last > profit_req * val, p_last, 0)
    else:
        arr = -np.where(p_last < profit_req * val, p_last, 0)
    p50 = round(np.count_nonzero(arr) / p_last.size, 2)

    # Returns Dictionary.
    # Calculating quantiles is expensive, so only uncomment if necessary.
    return {
        'symbol': key,
        'option_value_mc': val,  # Average value. Near Black Scholes Value.
        # 'value_quantile_5': np.percentile(p_last, 5),  # 5% chance below X
        # 'value_quantile_50': np.percentile(p_last, 50),  # 50% chance lands here.
        # 'value_quantile_95': np.percentile(p_last, 95),  # 5% chance above X.
        'probability_ITM': pop_ITM,  # Probability of ending ITM.
        'probability_of_50': p50,  # Probability of makeing half profit.
    }

def mc_numpy_vector(*args):
    """
    Monte Carlo simulations vectorized so that arrays work in calculations

    DEPRECATED: It's faster to multithread this operation.
    """
    curr_price, opt, ten_yr, rng, montecarlo_iterations = args
    vector_monte_carlo = np.vectorize(monte_carlo)
    _pop = vector_monte_carlo(
        key=opt['contractSymbol'],
        S=curr_price,
        K=opt['strikePrice'].to_numpy(),
        T=opt['daysToExpiration'].to_numpy(),
        rf=ten_yr,
        iv=opt['volatility'].to_numpy(),
        option_type=opt['option_type'].to_numpy(),
        rng=rng,
        n=montecarlo_iterations
    )
    return pd.DataFrame.from_records(_pop)  # pd.json_normalize(_pop.T)

def mc_multi_threading(*args):
    """
    Monte Carlo simulations vectorized so that arrays work in calculations.
    Multithreaded, means one CPU works multiple I/O.

    :param args: Passing all parameters from call.
    :return: Dataframe with results. Including a key to join later.
    """
    def threader(opt, ten_yr, rng, montecarlo_iterations):
        _pop = vector_monte_carlo(
            key=opt.index.get_level_values('symbol').to_numpy(),
            S=opt['lastPrice'].to_numpy(),
            K=opt['strikePrice'].to_numpy(),
            T=opt['daysToExpiration'].to_numpy(),
            rf=ten_yr,
            iv=opt['volatility'].to_numpy(),
            option_type=opt.index.get_level_values('option_type').to_numpy(),
            rng=rng,
            n=montecarlo_iterations
        )
        rez.append(_pop)

    rez = []  # List of dictionaries
    _opt, _ten_yr, _rng, _montecarlo_iterations, _chunks = args
    vector_monte_carlo = np.vectorize(monte_carlo)

    # Chunking tables in groups of 'chunks' values. Each a separate thread.
    dtes = _opt['daysToExpiration'].unique()  # List of DTE's
    d_chunk = [dtes[i:i + _chunks] for i in range(0, len(dtes), _chunks)]
    df_chunks = [_opt[(_opt['daysToExpiration'].isin(dte))] for dte in d_chunk]

    # Multi-threading.
    threads = []
    for df in df_chunks:
        arg = (df, _ten_yr, _rng, _montecarlo_iterations)
        t = threading.Thread(target=threader, args=arg)
        threads.append(t)

    [thread.start() for thread in threads]  # Kickoff threading.
    [thread.join() for thread in threads]  # Stop all threads.

    # Flatten list
    _result = []
    for i in range(len(rez)):
        for j in rez[i]:
            _result.append(j)

    return pd.DataFrame.from_records(_result)

class Modeling:
    def __init__(self, con, option_df):
        self.options = option_df.options  # Options coming in.
        self.quote = None  # All quote data.
        self.rf = 0  # Risk-free rate, i.e. 10-yr t-bill for modeling.
        self.prepare_tables(con)

    def get_quotes(self, con):
        """
        Get current price data from TDA

        Source: https://developer.tdameritrade.com/quotes/apis/get/marketdata/quotes

        :return: Current underlying stock price merged into the options table.
        """
        import httpx
        tickers = self.options.index.get_level_values('stock').unique().to_list()
        q = con.client.get_quotes(tickers)
        assert q.status_code == httpx.codes.OK, q.raise_for_status()

        prep = [v for k, v in q.json().items()]
        self.quote = pd.DataFrame.from_dict(prep)
        self.quote.rename(columns={'symbol': 'stock'}, inplace=True)

    def get_last_price(self, con):
        self.get_quotes(con)
        last_price = self.quote[[
            "stock",
            # "description",
            # "bidPrice",
            # "bidSize",
            # "bidId",
            # "askPrice",
            # "askSize",
            # "askId",
            "lastPrice",
            # "lastSize",
            # "lastId",
            # "openPrice",
            # "highPrice",
            # "lowPrice",
            # "closePrice",
            # "netChange",
            # "totalVolume",
            # "quoteTimeInLong",
            # "tradeTimeInLong",
            # "mark",
            # "exchange",
            # "exchangeName",
            # "marginable",
            # "shortable",
            # "volatility",
            # "digits",
            # "52WkHigh",
            # "52WkLow",
            # "peRatio",
            # "divAmount",
            # "divYield",
            # "divDate",
            # "securityStatus",
            # "regularMarketLastPrice",
            # "regularMarketLastSize",
            # "regularMarketNetChange",
            # "regularMarketTradeTimeInLong",
        ]]
        last_price.set_index('stock', inplace=True)

        # pd.merge(self.options, last_price, on='stock')
        self.options = self.options.join(last_price, on='stock')

    def get_risk_free_rate(self):
        # Get 10-yr risk-free rate from FRED.
        _url = "https://fred.stlouisfed.org/graph/fredgraph.csv?id=DGS10"
        _csv = pd.read_csv(_url)
        _value = _csv['DGS10'].values[-1]
        self.rf = float(_value) / 100

    def prepare_tables(self, con):
        self.get_last_price(con)
        self.get_risk_free_rate()

    def black_scholes(self):
        df = self.options.copy()
        if all(i in df.columns for i in ['putCall', 'symbol']):
            df = df.drop(columns=['putCall', 'symbol']).reset_index()
        # Black Scholes data here. Option value, greeks.
        bsch = black_scholes(
            S=df['lastPrice'].to_numpy(),
            K=df['strikePrice'].to_numpy(),
            T=df['daysToExpiration'].to_numpy(),
            rf=self.rf,
            iv=df['volatility'].to_numpy(),
            option_type=df['option_type'].to_numpy()
        )
        df = pd.concat([df, bsch], axis='columns')
        df.set_index(
            ['stock', 'option_type', 'symbol'],
            inplace=True
        )
        self.options = df
        return self.options

    def probability_of_profits(self, montecarlo_iterations):
        # Probability of profits.
        msg = f" {self.options.shape[0]} contracts" \
              f" x {montecarlo_iterations} iterations"

        # Generating random range is expensive, so doing it once.
        rng = np.random.Generator(np.random.PCG64())

        df = self.options.copy()

        # 1) Simple version.
        # Disabled because it's significantly slower than multithreading.
        # print("Numpy Vector" + msg)
        # pop = mc_numpy_vector(
        #     curr_price, opt, ten_yr, rng, montecarlo_iterations
        # )

        # 2) Multiple Threads.
        print(f"Multi Threading" + msg)
        # Chunks set to 2, is optimal after experimenting.
        chunks = 2  # How many DTE's to process per Thread.
        pop = mc_multi_threading(
            df,
            self.rf,
            rng,
            montecarlo_iterations,
            chunks
        )
        pop.set_index(['symbol'], inplace=True)

        if all(i in df.columns for i in ['putCall', 'symbol']):
            df = df.drop(columns=['putCall', 'symbol'])

        df.reset_index(inplace=True)
        df = df.join(pop, on='symbol')
        df.sort_values(by='symbol', inplace=True)
        df.set_index(['stock', 'option_type', 'symbol'], inplace=True)

        self.options = df
        return self.options


import numpy as np

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

def sigmoid_derivative(x):
    """
    Compute the gradient (also called the slope or derivative) of the sigmoid function with respect to its input x.
    You can store the output of the sigmoid function into variables and then use it to calculate the gradient.

    Arguments:
    x -- A scalar or numpy array

    Return:
    ds -- Your computed gradient.
    """

    s = sigmoid(x)
    ds = s * (1-s)

    return ds

def softmax(x):
    """Calculates the softmax for each row of the input x.

    Your code should work for a row vector and also for matrices of shape (m,n).

    Argument:
    x -- A numpy matrix of shape (m,n)

    Returns:
    s -- A numpy matrix equal to the softmax of x, of shape (m,n)
    """

    # Apply exp() element-wise to x. Use np.exp(...).
    x_exp = np.exp(x)

    # Create a vector x_sum that sums each row of x_exp. Use np.sum(..., axis = 1, keepdims = True).
    x_sum = np.sum(x_exp, axis =1, keepdims= True)

    # Compute softmax(x) by dividing x_exp by x_sum. It should automatically use numpy broadcasting.
    s = x_exp/x_sum
    return s

def normalizeRows(x):
    """
    Implement a function that normalizes each row of the matrix x (to have unit length).

    Argument:
    x -- A numpy matrix of shape (n, m)

    Returns:
    x -- The normalized (by row) numpy matrix. You are allowed to modify x.
    """

    x_norm = np.linalg.norm(x,axis=1,keepdims=True)
    x = x/x_norm
    return x

def L1(yhat, y):
    """
    Arguments:
    yhat -- vector of size m (predicted labels)
    y -- vector of size m (true labels)

    Returns:
    loss -- the value of the L1 loss function defined above
    """
    loss = np.sum(abs(y-yhat))
    return loss

def L2(yhat, y):
    """
    Arguments:
    yhat -- vector of size m (predicted labels)
    y -- vector of size m (true labels)

    Returns:
    loss -- the value of the L2 loss function defined above
    """
    loss = np.sum((y-yhat)**2)
    return loss


"""
Purpose of this file is to give examples of masking

This script is partially dirived from the LinkedIn learning course
https://www.linkedin.com/learning/numpy-data-science-essential-training/create-arrays-from-python-structures

"""
import numpy as np
# PART 2 - Boolean Mask Arrays
my_vector = np.array([-18, -4, 0, 2, 21, 37, 105])
print(f'my_vector is {my_vector}')

# create a boolean mask, based on whether an element is divisible by 3
zero_mod_3_mask = 0 == (my_vector % 3)
print(f'boolean mask is {zero_mod_3_mask}')
 #
# going create a sub array using the boolean mask
sub_array = my_vector[zero_mod_3_mask]
print(f'sub_array is {sub_array}')
# note that sub_array length is now 4
print(f'sub_array length is {sub_array.size}')

# we can also use subarray to create a new subarray
print(f'we can create sub array by using sub_array[sub_array>0] to get {sub_array[sub_array>0]}')

# another way to this
mod_test = 0 == (my_vector % 3)
pos_test = my_vector > 0
# we can use the method
combined_mask = np.logical_and(mod_test, pos_test)

print('finding sub array of elements that are %3 and >0 is {my_vector[combined_mask]}')


"""
identity

The identity tool returns an identity array. An identity array is a square matrix with all the main diagonal elements as  and the rest as . The default type of elements is float.

import numpy
print numpy.identity(3) #3 is for  dimension 3 X 3

#Output
[[ 1.  0.  0.]
 [ 0.  1.  0.]
 [ 0.  0.  1.]]
eye

The eye tool returns a 2-D array with 's as the diagonal and 's elsewhere. The diagonal can be main, upper or lower depending on the optional parameter . A positive  is for the upper diagonal, a negative  is for the lower, and a   (default) is for the main diagonal.

import numpy
print numpy.eye(8, 7, k = 1)    # 8 X 7 Dimensional array with first upper diagonal 1.

#Output
[[ 0.  1.  0.  0.  0.  0.  0.]
 [ 0.  0.  1.  0.  0.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  0.  0.  1.]
 [ 0.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.]]

print numpy.eye(8, 7, k = -2)   # 8 X 7 Dimensional array with second lower diagonal 1.
Task

Your task is to print an array of size X with its main diagonal elements as 's and 's everywhere else.

Note

In order to get alignment correct, please insert the line  below the numpy import.

Input Format

A single line containing the space separated values of  and .
 denotes the rows.
 denotes the columns.

Output Format

Print the desired X array.

Sample Input

3 3
Sample Output

[[ 1.  0.  0.]
 [ 0.  1.  0.]
 [ 0.  0.  1.]]
"""

import numpy as np

np.set_printoptions(legacy="1.13")

N, M = list(map(int, input().split()))
print(np.eye(N, M, 0))


import math
import numpy as np

def safe_slope(x1: float, y1: float, x2: float, y2: float) -> float:
    """Calulates the slope between two points in a way tha avoids division by zeros."""
    x_diff = (x2 - x1)

    if x_diff == 0:
        # Avoid division by zero by return inf.
        return np.inf

    return (y2 - y1) / x_diff

def z_axis_angle(x1: float, y1: float, x2: float, y2: float) -> float:
    """Calulates that angle in degrees [0, 360] that the slope of the provided points
    form with rotation about the z axis."""

    angle = math.degrees(math.atan2(y2 - y1, x2 - x1))

    # Wrap angle to bound it between 0 and 360.
    if angle < 0:
        angle += 360

    if angle > 360:
        angle -= 360

    return angle

def euclidean_distance(x1: float, y1: float, x2: float, y2: float) -> float:
    """Calculates the euclidean distance between two points."""
    return math.sqrt(((x1 - x2)**2) + ((y1 - y2)**2))

def wrap_heading(current_heading: float, new_heading: float) -> float:
    """Determines the offset between the current and provided heading to adjust
    them such that the normal subtraction between them results in an angle less than
    or equal to 180 degrees."""
    if abs(new_heading - current_heading) > 180:
      if new_heading > current_heading:
        new_heading -= 360

      else:
        new_heading += 360

    return new_heading

def angular_distance(a1: float, a2: float) -> float:
    """Calculates the angular distance between two angles in degrees."""
    phi = abs(a2 - a1) % 360;
    distance = 360 - phi if phi > 180 else phi;
    return distance;


# Ejercicio 1

import numpy as np
import matplotlib.pylab as plt

# Use esta funcion que recibe un valor x y retorna un valor f(x) donde f es la forma funcional que debe seguir su distribucion.
def mifun(x):
    x_0 = 3.0
    a = 0.01
    return np.exp(-(x**2))/((x-x_0)**2 + a**2)

# Dentro de una funcion que reciba como parametros el numero de pasos y el sigma de la distribucion gausiana que va a usar para calcular el paso de su caminata, implemente el algortimo de Metropolis-Hastings. Finalmente, haga un histograma de los datos obtenidos y grafique en la misma grafica, la funcion de distribucion de probabilidad fx (Ojo, aca debe normalizar). Guarde la grafica sin mostrarla en un pdf. Use plt.savefig("histograma_"+str(sigma)+"_"+str(pasos)+".pdf"), donde sigma y pasos son los parametros que recibe la funcion.

def metropolis(x,f,N,sigma):
    datos=np.zeros(N)
    datos[0]=np.random.random()*max(x)
    for i in range(1,N):
        xold=datos[i-1]
        xnew=np.random.normal(xold,sigma)
        alpha=f(xnew)/f(xold)
        if(alpha>1):
            datos[i]=xnew
        else:
            beta=np.random.random()
            if(beta<alpha):
                datos[i]=xnew
            else:
                datos[i]=xold
    return datos

# Cuando haya verificado que su codigo funciona, use los siguientes parametros:
# sigma = 5, pasos =100000
# sigma = 0.2, pasos =100000
# sigma = 0.01, pasos =100000
# sigma = 0.1, pasos =1000
# sigma = 0.1, pasos =100000
# este puede ser muy demorado dependiendo del computador: sigma = 0.1, pasos =500000
sigma=[5,0.2,0.01,0.1,0.1,0.1]
pasos=[100000,100000,100000,1000,100000,500000]

# Al ejecutar el codigo, este debe generar 6 (o 5) graficas .pdf una para cada vez que se llama a la funcion.

x=np.linspace(-4,4,1000)
y=mifun(x)/np.sum(mifun(x)*(x[1]-x[0]))

for i in range(len(pasos)):
    s=sigma[i]
    N=pasos[i]
    MH=metropolis(x,mifun,N,s)

    plt.figure()
    plt.hist(MH,bins=100,density=True,label="MH")
    plt.plot(x,y,label="Función")
    plt.xlabel("x")
    plt.ylabel("y")
    plt.legend()
    plt.title('Metropolis con $\sigma$ ='+str(s)+' y '+str(N)+ ' pasos.')
    plt.savefig("histograma_"+str(s)+"_"+str(N)+".png")
    plt.close()


import numpy as np

### Functions for you to fill in ###
##This is the Polynomial Kernel Exercise

def polynomial_kernel(X, Y, c, p):
    """
        Compute the polynomial kernel between two matrices X and Y::
            K(x, y) = (<x, y> + c)^p
        for each pair of rows x in X and y in Y.

        Args:
            X - (n, d) NumPy array (n datapoints each with d features)
            Y - (m, d) NumPy array (m datapoints each with d features)
            c - a coefficient to trade off high-order and low-order terms (scalar)
            p - the degree of the polynomial kernel

        Returns:
            kernel_matrix - (n, m) Numpy array containing the kernel matrix
    """
    #X= X.shape
    #Y= Y.shape
    polynomial_kernel= (c + np.dot(X, Y.T)) ** p
    return polynomial_kernel

def rbf_kernel(X, Y, gamma):
    """
        Compute the Gaussian RBF kernel between two matrices X and Y::
            K(x, y) = exp(-gamma ||x-y||^2)
        for each pair of rows x in X and y in Y.

        Args:
            X - (n, d) NumPy array (n datapoints each with d features)
            Y - (m, d) NumPy array (m datapoints each with d features)
            gamma - the gamma parameter of gaussian function (scalar)

        Returns:
            kernel_matrix - (n, m) Numpy array containing the kernel matrix
    """
    m,d = Y.shape
    X= np.resize(X, (m,d))
    gaussian_kernel= np.exp(-1*np.linalg.norm(X-Y)**2/ (2 * (gamma ** 2)))
    return gaussian_kernel
# =============================================================================
#
#     K = np.zeros((X.shape[0],Y.shape[0]))
#     for i,x in enumerate(X):
#         for j,y in enumerate(Y):
#             K[i,j] = np.exp(-1*np.linalg.norm(x-y)**2/ (2 * (gamma ** 2)))
#     return K
# =============================================================================

Machine-Learning-with-Python- From-LM-to-DL/Unit 2. Non Linear Classification, Linear Regression, Colaborative Filtering/mnist/part1/kernel.py


import numpy as np
import matplotlib.pyplot as plt

np.random.seed(100)
X = np.arange(1, 51, 1).reshape(50, 1)
u = np.random.uniform(-1, 1, (50, 1))
Y = u + X

X_temp = np.vstack((np.ones((1, 50)), X.T))
X_psuedo_inverse = np.matmul(X_temp.T, np.linalg.inv(np.matmul(X_temp, X_temp.T)))

W = np.matmul(Y.reshape(1, 50), X_psuedo_inverse)

f_X = W[0][1]*X[:, 0] + W[0][0]
plt.plot(X[:, 0], Y[:, 0], 'o')
plt.plot(X[:, 0], f_X, 'r', label = 'Line to fit data')
plt.xlabel("X-axis")
plt.ylabel("Y-axis")
plt.title("Linear Least Squares Fit")
plt.legend()
plt.show()

w = np.array([0.15, 0.6])
eta = 0.00001

f_X = w[1]*X[:, 0] + w[0]
plt.plot(X[:, 0], Y[:, 0], 'o')
plt.plot(X[:, 0], f_X, 'r', label = 'Line to fit data')
plt.xlabel("X-axis")
plt.ylabel("Y-axis")
plt.title("Gradient Descent Fit before training")
plt.legend()
plt.show()
print(w, eta)

z = 0
while True:
    z+=1
    grad = np.zeros((2,))
    for i in range(50):
        grad[0] += (Y[i, 0] - w[0] - w[1]*X[i, 0])
    grad[0] *= -2

    for i in range(50):
        grad[1] += (Y[i, 0] - w[0] - w[1]*X[i, 0])*X[i, 0]
    grad[1] *= -2

    delta_w = eta * grad
    new_w = w - delta_w

    if np.linalg.norm(w - new_w) < 0.0001:
        break
    else:
        w = new_w
        wtemp = new_w
    if z == 1:
        f_X = w[1]*X[:, 0] + w[0]
        plt.plot(X[:, 0], Y[:, 0], 'o')
        plt.plot(X[:, 0], f_X, 'r', label = 'Line to fit data')
        plt.xlabel("X-axis")
        plt.ylabel("Y-axis")
        plt.title("Gradient Descent Fit after 1 epoch")
        plt.legend()
        plt.show()

f_X = w[1]*X[:, 0] + w[0]
plt.plot(X[:, 0], Y[:, 0], 'o')
plt.plot(X[:, 0], f_X, 'r', label = 'Line to fit data')
plt.xlabel("X-axis")
plt.ylabel("Y-axis")
plt.title(f"Gradient Descent Fit after {z} epochs")
plt.legend()
plt.show()

print("DIFF", W - w.reshape(1, 2))


from math import fabs as fabs
from math import floor as floor
from math import sqrt as sqrt
from scipy.special import erfc as erfc
from scipy.special import gammaincc as gammaincc

class FrequencyTest:

    @staticmethod
    def monobit_test(binary_data:str, verbose=False):
        """
        The focus of the test is the proportion of zeroes and ones for the entire sequence.
        The purpose of this test is to determine whether the number of ones and zeros in a sequence are approximately
        the same as would be expected for a truly random sequence. The test assesses the closeness of the fraction of
        ones to 陆, that is, the number of ones and zeroes in a sequence should be about the same.
        All subsequent tests depend on the passing of this test.

        if p_value < 0.01, then conclude that the sequence is non-random (return False).
        Otherwise, conclude that the the sequence is random (return True).

        :param      binary_data         The seuqnce of bit being tested
        :param      verbose             True to display the debug messgae, False to turn off debug message
        :return:    (p_value, bool)     A tuple which contain the p_value and result of frequency_test(True or False)

        """

        length_of_bit_string = len(binary_data)

        # Variable for S(n)
        count = 0
        # Iterate each bit in the string and compute for S(n)
        for bit in binary_data:
            if bit == '0':
                # If bit is 0, then -1 from the S(n)
                count -= 1
            elif bit == '1':
                # If bit is 1, then +1 to the S(n)
                count += 1

        # Compute the test statistic
        sObs = count / sqrt(length_of_bit_string)

        # Compute p-Value
        p_value = erfc(fabs(sObs) / sqrt(2))

        if verbose:
            print('Frequency Test (Monobit Test) DEBUG BEGIN:')
            print("\tLength of input:\t", length_of_bit_string)
            print('\t# of \'0\':\t\t\t', binary_data.count('0'))
            print('\t# of \'1\':\t\t\t', binary_data.count('1'))
            print('\tS(n):\t\t\t\t', count)
            print('\tsObs:\t\t\t\t', sObs)
            print('\tf:\t\t\t\t\t',fabs(sObs) / sqrt(2))
            print('\tP-Value:\t\t\t', p_value)
            print('DEBUG END.')

        # return a p_value and randomness result
        return (p_value, (p_value >= 0.01))

    @staticmethod
    def block_frequency(binary_data:str, block_size=128, verbose=False):
        """
        The focus of the test is the proportion of ones within M-bit blocks.
        The purpose of this test is to determine whether the frequency of ones in an M-bit block is approximately M/2,
        as would be expected under an assumption of randomness.
        For block size M=1, this test degenerates to test 1, the Frequency (Monobit) test.

        :param      binary_data:        The length of each block
        :param      block_size:         The seuqnce of bit being tested
        :param      verbose             True to display the debug messgae, False to turn off debug message
        :return:    (p_value, bool)     A tuple which contain the p_value and result of frequency_test(True or False)
        """

        length_of_bit_string = len(binary_data)

        if length_of_bit_string < block_size:
            block_size = length_of_bit_string

        # Compute the number of blocks based on the input given.  Discard the remainder
        number_of_blocks = floor(length_of_bit_string / block_size)

        if number_of_blocks == 1:
            # For block size M=1, this test degenerates to test 1, the Frequency (Monobit) test.
            return FrequencyTest.monobit_test(binary_data[0:block_size])

        # Initialized variables
        block_start = 0
        block_end = block_size
        proportion_sum = 0.0

        # Create a for loop to process each block
        for counter in range(number_of_blocks):
            # Partition the input sequence and get the data for block
            block_data = binary_data[block_start:block_end]

            # Determine the proportion 蟺i of ones in each M-bit
            one_count = 0
            for bit in block_data:
                if bit == '1':
                    one_count += 1
            # compute π
            pi = one_count / block_size

            # Compute Σ(πi -½)^2.
            proportion_sum += pow(pi - 0.5, 2.0)

            # Next Block
            block_start += block_size
            block_end += block_size

        # Compute 4M Σ(πi -½)^2.
        result = 4.0 * block_size * proportion_sum

        # Compute P-Value
        p_value = gammaincc(number_of_blocks / 2, result / 2)

        if verbose:
            print('Frequency Test (Block Frequency Test) DEBUG BEGIN:')
            print("\tLength of input:\t", length_of_bit_string)
            print("\tSize of Block:\t\t", block_size)
            print('\tNumber of Blocks:\t', number_of_blocks)
            print('\tCHI Squared:\t\t', result)
            print('\t1st:\t\t\t\t', number_of_blocks / 2)
            print('\t2nd:\t\t\t\t', result / 2)
            print('\tP-Value:\t\t\t', p_value)
            print('DEBUG END.')

        return (p_value, (p_value >= 0.01))


from matplotlib import markers
import numpy as np
import matplotlib.pyplot as plt

# Langrange Polynomial of n^th Degree - The Degree is Specified by the Data we Use
def P(x_data, f_data, pos, x):  # pos is an array with elements the positions of x_data we use

    # n = Degree of the polynomial + 1
    n = len(pos)

    def L(x_data, pos, x):

        L = np.ones(n)

        # Polynomial that goes through x_data[pos[i]]
        for i in range(n):
            for j in range(n):
                if pos[i] != pos[j]:
                    L[i] *= (x - x_data[pos[j]]) / (x_data[pos[i]] - x_data[pos[j]])
        return L

    P = 0.0

    for i in range(n):
            P += f_data[pos[i]] * L(x_data, pos, x)[i]

    return P

# Function to Generate Points for Graphs
def graph(pos, n = 150):
    x_graph = np.linspace(x_data[np.amin(pos)], x_data[np.amax(pos)], n)
    y_graph = np.zeros(len(x_graph))

    i = 0

    for x in x_graph:
        y_graph[i] += [P(x_data, f_data, pos, x)]
        i += 1

    return x_graph, y_graph

if __name__ == "__main__":
    # Some Data
    x_data = [-1.6, -1.0, -0.4, 0.2, 0.8, 1.4]
    f_data = [0.278037, 0.606531, 0.923116, 0.980199, 0.726149, 0.375311]

    print('\nCompuated with Lagrange Polynomials of 3^rd Degree:')
    print(f'\tf(0) = {P(x_data, f_data, [1, 2, 3, 4], 0):.6f}')
    print(f'\tf(1) = {P(x_data, f_data, [2, 3, 4, 5], 1):.6f}')

    print('\nCompuated with Lagrange Polynomials of 4^th Degree:')
    print(f'\tf(0) = {P(x_data, f_data, [1, 2, 3, 4, 5], 0):.6f}')
    print(f'\tf(1) = {P(x_data, f_data, [1, 2, 3, 4, 5], 1):.6f}')

    # Visualize the Lagrange Polynomials
    plt.subplot(1, 1, 1)
    plt.plot(graph([1, 2, 3, 4])[0], graph([1, 2, 3, 4])[1], linewidth = 1, label = 'Third Degree with pos = [1, 2, 3, 4]')
    plt.plot(graph([2, 3, 4, 5])[0], graph([2, 3, 4, 5])[1], linewidth = 1, label = 'Third Degree with pos = [2, 3, 4, 5]')
    plt.plot(graph([1, 2, 3, 4, 5])[0], graph([1, 2, 3, 4, 5])[1], linewidth = 1, label = 'Fourth Degree with pos = [1, 2, 3, 4, 5]')
    plt.scatter(x_data, f_data, color = 'black', marker = ".", label = 'Sample Points')

    plt.legend(loc = 'lower center', prop = {'size': 10})
    plt.ylabel('$y$')
    plt.xlabel('$x$')
    plt.title('Lagrange Method')
    plt.show()


def approx_first_derivative(f,x,h):
    """
    Numerical differentiation by finite differences. Uses central point formula
    to approximate first derivative of function.
    Args:
        f (function): function definition.
        x (float): point where first derivative will be approximated
        h (float): step size for central differences. Tipically less than 1
    Returns:
        df (float): approximation to first_derivative.
    """
    df = (f(x+h) - f(x-h))/(2.0*h)
    return df

def approx_second_derivative(f,x,h):
    """
    Numerical differentiation by finite differences. Uses central point formula
    to approximate second derivative of function.
    Args:
        f (function): function definition.
        x (float): point where second derivative will be approximated
        h (float): step size for central differences. Tipically less than 1
    Returns:
        ddf (float): approximation to second_derivative.
    """
    ddf =(f(x+h) - 2.0*f(x) + f(x-h))/h**2
    return ddf

def main_function():
    """
    Main execution function
      args:
        -
      returns:
        -
    """

    ## Python libraries
    import numpy as np

    ## Parameters
    f = np.arctan
    x = 0.9
    h = 1e-6

    ## Main code
    res_first_d = approx_first_derivative(f,x,h)
    res_second_d = approx_second_derivative(f,x,h)

    print(res_first_d)
    print(res_second_d)

if __name__ == "__main__":
    main_function()


# based on https://rpubs.com/andersgs/my_solutions_chapter2_statrethink
# P(twins|A) = 10%
p_twins_a = 0.1
# P(single infant|A) = 90%
p_single_a = 0.9
# P(twins|B) = 20%
p_twins_b = 0.2
# P(single infant| B) = 80%
p_single_b = 0.8
# P(A) = P(B) = 0.5\
p_a, p_b = 0.5, 0.5

# P(twins) = P(twins|A)*P(A) + P(twins|B)*P(B)
p_twins = p_twins_a * p_a + p_twins_b * p_b

# P(A|twins) = P(twins|A)*P(A)/P(twins)
p_a_twins = p_twins_a * p_a / p_twins

# P(B|twins) = P(twins|B)*P(B)/P(twins)
p_b_twins = p_twins_b * p_b / p_twins

#
result = p_a_twins * p_twins_a + p_b_twins * p_twins_b

print('result 2h1 = %f' % (result))

# 2h2 - P(A|twins)
print('result 2h2 = %f' % (p_a_twins))

# 2h3
# P(A | twins, single) = P(twins|A)*P(single|A)*P(A)/P(twins,single)
# P(twins,single) = P(twins|A)*P(single|A)*P(A) + P(twins|B)*P(single|B)*P(B)
p_twins_single = p_twins_a * p_single_a * p_a + p_twins_b * p_single_b * p_b
p_a_twins_single = p_twins_a * p_single_a * p_a / p_twins_single
print('result 2h3=%f' % p_a_twins_single)

# 2h4
p_test_a_a = 0.8
p_test_b_a = 0.2
p_test_b_b = 0.65
p_test_a_b = 0.35

# P(A|testA) = P(testA|A)*P(A)/P(testA)
# P(testA) = P(testA|A)*P(A)+P(testA|B)*P(B)
p_test_a = p_test_a_a * p_a + p_test_a_b * p_b
p_a_test_a = p_test_a_a * p_a / p_test_a
print("result 2h4_1=%f" % p_a_test_a)

# P(A|testA,twins,single) = P(testA|A)*P(twins|A)*P(single|A)*P(A)/P(testA,twins,single)
# P(testA,twins,single) =
#   P(testA|A)*P(twins|A)*P(single|A)*P(A) +
#   P(testA|B)*P(twins|B)*P(single|B)*P(B)

p_test_a_twins_single = p_test_a_a * p_twins_a * p_single_a * p_a + p_test_a_b * p_twins_b * p_single_b * p_b
p_a_testa_twins_single = p_test_a_a * p_twins_a * p_single_a * p_a/p_test_a_twins_single
print('result 2h4_2=%f'%p_a_testa_twins_single)

print('---different approach---')

# based on https://github.com/cavaunpeu/statistical-rethinking/blob/master/chapter-2/homework.R
import numpy as np

like = np.array([p_twins_a, p_twins_b])
prior = np.repeat(1., 2)
post = like * prior
post = post / post.sum()
post_res = post.dot(like)
print("result 2h1=%f" % post_res)
print("result 2h2=%f" % post[0])

like = np.array([p_single_a, p_single_b])
# last posterior is new prior
post = like * post
post = post / post.sum()
print('result 2h3=%f' % post[0])

#2h4-1
like = np.array([p_test_a_a, p_test_a_b])
prior = np.repeat(1., 2)
post = like * prior
post = post / post.sum()
print("result 2h4_1=%f" % post[0])

#2h4-2
# first resolve post from birthing twins
like = np.array([p_twins_a, p_twins_b])
prior = np.repeat(1., 2)
post = like * prior
post = post / post.sum()
like = np.array([p_single_a, p_single_b])
# then resolve posterior after birthing also single
post = like * post
post = post / post.sum()
# then after these events, also resolve the event on testing for A
like = np.array([p_test_a_a, p_test_a_b])
post = like * post
post = post / post.sum()
print("result 2h4_2=%f" % post[0])


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

import numpy as np

# Table Utilities

def is_associative(table):
    indices = range(len(table))
    result = True
    for a in indices:
        for b in indices:
            for c in indices:
                ab = table[a][b]
                bc = table[b][c]
                if not (table[ab][c] == table[a][bc]):
                    result = False
                    break
    return result

def is_commutative(table):
    indices = range(len(table))
    result = True
    for a in indices:
        for b in indices:
            if table[a][b] != table[b][a]:
                result = False
                break
    return result

def has_left_identity(table):
    indices = range(len(table))
    identity = None
    for x in indices:
        if all(table[x][y] == y for y in indices):
            identity = x
            break
    return identity

def has_right_identity(table):
    indices = range(len(table))
    identity = None
    for x in indices:
        if all(table[y][x] == y for y in indices):
            identity = x
            break
    return identity

def has_identity(table):
    left_id = has_left_identity(table)
    right_id = has_right_identity(table)
    if (left_id is not None) and (right_id is not None):
        return left_id
    else:
        return None

def has_inverses(table):
    return False

def inverse_lookup_dict(table, identity):
    elements = range(len(table))
    row_indices, col_indices = np.where(table == identity)
    return {elements[elem_index]: elements[elem_inv_index]
            for (elem_index, elem_inv_index)
            in zip(row_indices, col_indices)}

if __name__ == '__main__':

    print("\n=======================================================================")

    print("\n--------------")
    print("START OF TESTS")
    print("--------------")

    import pprint as pp

    # Table Tests

    print("\nTable Tests:\n")

    # not assoc; is comm; no identity -- the RPS magma table, above
    tbl1 = [[0, 1, 0], [1, 1, 2], [0, 2, 2]]

    # is assoc; not comm; has identity (0) --- the S3 group table
    tbl2 = [[0, 1, 2, 3, 4, 5], [1, 2, 0, 5, 3, 4], [2, 0, 1, 4, 5, 3],
            [3, 4, 5, 0, 1, 2], [4, 5, 3, 2, 0, 1], [5, 3, 4, 1, 2, 0]]

    # is assoc; is comm; has identity (0) --- the Z4 group table
    tbl3 = [[0, 1, 2, 3], [1, 2, 3, 0], [2, 3, 0, 1], [3, 0, 1, 2]]

    # powerset(3) group table
    tbl4 = [[0, 1, 2, 3, 4, 5, 6, 7], [1, 0, 4, 5, 2, 3, 7, 6], [2, 4, 0, 6, 1, 7, 3, 5],
            [3, 5, 6, 0, 7, 1, 2, 4], [4, 2, 1, 7, 0, 6, 5, 3], [5, 3, 7, 1, 6, 0, 4, 2],
            [6, 7, 3, 2, 5, 4, 0, 1], [7, 6, 5, 4, 3, 2, 1, 0]]

    tbl5 = [[0, 3, 0, 3, 0, 3], [1, 4, 1, 4, 1, 4], [2, 5, 2, 5, 2, 5],
            [3, 0, 3, 0, 3, 0], [4, 1, 4, 1, 4, 1], [5, 2, 5, 2, 5, 2]]

    test_tables = [tbl1, tbl2, tbl3, tbl4, tbl5]

    for tbl in test_tables:
        pp.pprint(tbl)
        print()

    print("   Table     Associative?  Commutative?   Left Id?   Right Id?  Identity?")
    print('-' * 75)
    for tbl in test_tables:
        i = test_tables.index(tbl) + 1
        is_assoc = str(is_associative(tbl))
        is_comm = str(is_commutative(tbl))
        lft_id = str(has_left_identity(tbl))
        rgt_id = str(has_right_identity(tbl))
        ident = str(has_identity(tbl))
        print(f"{i :>{6}} {is_assoc :>{14}} {is_comm :>{12}} {lft_id :>{12}} {rgt_id :>{12}} {ident :>{10}}")

    print("\n------------")
    print("END OF TESTS")
    print("------------")


import numpy as np

rad = np.pi/2

arr_rad = np.array([np.pi/2, np.pi/3, np.pi/4, np.pi/5])

### 1.

# 1.
print(np.sin(rad))
print(np.sin(arr_rad))

# 2.
print(np.cos(rad))
print(np.cos(arr_rad))

# 3.
print(np.tan(rad))
print(np.tan(arr_rad))

### 2.

# 1.
print(np.rad2deg(rad))
print(np.rad2deg(arr_rad))

# 2.
print(np.deg2rad(180))
print(np.deg2rad([360, 180, 90, 270]))

### 3.

# 1.
print(np.arcsin(1.0))
print(np.arcsin([1.0, 0.8660254, 0.70710678, 0.58778525]))

# 2.
print(np.arccos(6.123233995736766e-17))
print(np.arccos([6.12323400e-17, 5.00000000e-01, 7.07106781e-01, 8.09016994e-01]))

# 3.
print(np.arctan(1.633123935319537e+16))
print(np.arctan([1.63312394e+16, 1.73205081e+00, 1.00000000e+00, 7.26542528e-01]))

###

base = 5
perp = 8

print(np.hypot(base, perp))

Tutorial/Code/38. NumPy Trigonometric Functions.py


import numpy
from numpy import linalg as LA

# Input the matrix X
x = numpy.matrix([[5.1, 160, 82000],
                  [5.2, 170, 84000],
                  [5.3, 180, 86000],
                  [5.4, 190, 88000],
                  [5.5, 200, 90000],
                  [5.6, 110, 81000],
                  [5.7, 120, 83000],
                  [5.8, 130, 85000],
                  [5.9, 140, 87000],
                  [6.0, 150, 89000]])

print("Input Matrix = \n", x)

print("Number of Dimensions = ", x.ndim)

print("Number of Rows = ", numpy.size(x,0))
print("Number of Columns = ", numpy.size(x,1))

xtx = x.transpose() * x
print("t(x) * x = \n", xtx)

# Eigenvalue decomposition
evals, evecs = LA.eigh(xtx)
print("Eigenvalues of x = \n", evals)
print("Eigenvectors of x = \n",evecs)

# Here is the transformation matrix
dvals = 1.0 / numpy.sqrt(evals)
transf = evecs * numpy.diagflat(dvals)
print("Transformation Matrix = \n", transf)

# Here is the transformed X
transf_x = x * transf;
print("The Transformed x = \n", transf_x)

# Check columns of transformed X
xtx = transf_x.transpose() * transf_x;
print("Expect an Identity Matrix = \n", xtx)

# Orthonormalize using the orth function
import scipy
from scipy import linalg as LA2

orthx = LA2.orth(x)
print("The orthonormalize x = \n", orthx)

# Check columns of the ORTH function
check = orthx.transpose().dot(orthx)
print("Also Expect an Identity Matrix = \n", check)

import numpy,from numpy,import scipy,from scipy


#!/usr/bin/env python3
import sys
import numpy as np

np.random.seed(2017)

def inside_circle(total_count):

    x = np.random.uniform(size=total_count)
    y = np.random.uniform(size=total_count)

    radii_square = x**2 + y**2

    count = (radii_square<=1.0).sum()

    return count

def estimate_pi(n_samples):

    return (4.0 * inside_circle(n_samples) / n_samples)

if __name__=='__main__':

    n_samples = 10000
    if len(sys.argv) > 1:
        n_samples = int(sys.argv[1])

    my_pi = estimate_pi(n_samples)
    sizeof = np.dtype(np.float64).itemsize

    print("[serial version] required memory {:.3f} MB".format(n_samples*sizeof*3/(1024*1024)))
    print("[serial version] pi is {} from {} samples".format(my_pi,n_samples))
    print("[serial version] got {} digits right".format(int(np.ceil(-np.log10(abs(my_pi - np.pi))))))


import numpy as np

'''
Exercise 0 (ungraded).
One reason to consider Numpy is that it "can be much faster," as noted above.
But how much faster is that? Run the experiment below to see.
'''

n = 1000000

L = range(n)
%timeit [i**2 for i in L]
# 269 ms ± 729 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

np.arange(10)  # Moral equivalent to `range`

A = np.arange(n)
%timeit A**2

#699 µs ± 5.51 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

'''
Exercise 1 (ungraded).
The following code creates an identity matrix in two different ways, which are found to be equal according to the assertion.
But in fact there is a subtle difference between the I and I_u matrices created below; can you spot it?
'''
n = 3
I = np.eye(n)

print("==> I = eye(n):")
print(I)

u = [1] * n
I_u = np.diag(u)

print("\n==> u:\n", u)
print("==> I_u = diag (u):\n", I_u)

assert np.all(I_u == I)

#Difference is between types, eyes is a float-point numbers, while daig is integer.

'''
Exercise 2 (5 points). Consider the following  6×66×6  matrix, which has 4 different subsets highlighted.
For each subset illustrated above, write an indexing or slicing expression that extracts the subset.
Store the result of each slice into Z_green, Z_red, Z_orange, and Z_cyan.
'''

Z= np.array([[0,1,2,3,4,5],[10,11,12,13,14,15],[20,21,22,23,24,25],[30,31,32,33,34,35],[40,41,42,43,44,45],[50,51,52,53,54,55]])

# Construct `Z_green`, `Z_red`, `Z_orange`, and `Z_cyan`:
Z_orange = Z[0, 3:5]
Z_red = Z[:, 2]
Z_green = Z[2::2, ::2]
Z_cyan = Z[4:, 4:]

'''
Exercise 3 (1 point).
Given the input array, x[:], above, create an array, mask_mult_3[:] such that mask_mult_3[i] is true only if x[i] is a positive multiple of 3.
'''

mask_mult_3 = (x > 0) & (x % 3 == 0)

'''
Exercise 4 (3 points). Complete the prime number sieve algorithm, which is illustrated below.
'''

from math import sqrt

def sieve(n):
    """
    Returns the prime number 'sieve' shown above.

    That is, this function returns an array `X[0:n+1]`
    such that `X[i]` is true if and only if `i` is prime.
    """
    is_prime = np.empty(n + 1, dtype=bool)  # the "sieve"

    # Initial values
    is_prime[0:2] = False  # {0, 1} are _not_ considered prime
    is_prime[2:] = True  # All other values might be prime

    # Implement the sieving loop

    for k in range(2, int(sqrt(n)) + 1):
        is_prime[2 * k::k] = False

    return is_prime

# Prints your primes
print("==> Primes through 20:\n", np.nonzero(sieve(20))[0])

EdX/GTx: CSE6040x: FA18 - Computing for Data Analysis/Module 2: The analysis of data/Topic 10: Numerical computing with Numpy-Scipy/Notebook_10.py


import numpy as np
import pandas as pd
import scipy
from scipy.stats import norm

def semi_deviation(r):
    """
    Returns the semi deviation, aka negative semi deviation of r
    r must be a Series or a DataFrame
    """
    is_negative = r < 0
    return r[is_negative].std(ddof=0)

def skewness(r):
    """
    Alternate to scipy.stats.skew()
    Computes the skewness of the supplied Series or  DataFrame
    Returns a float or a series
    """
    demeaned_r = r - r.mean()
    sigma_r = r.std(ddof=0)
    exp = (demeaned_r ** 3).mean()
    return exp / sigma_r ** 3

def kurtosis(r):
    """
    Alternate to scipy.stats.kurtosis()
    Computes the kurtosis of the supplied Series or DataFrame
    Returns a float or a series
    """
    demeaned_r = r - r.mean()
    sigma_r = r.std(ddof=0)
    exp = (demeaned_r ** 4).mean()
    return exp / sigma_r ** 4

def is_normal(r, level=0.01):
    """
    Applies the Jarque-Bera test to determine if a Series is normal or not.
    Test is applied at 1% level by default
    Returns True if the hypothesis of normality is accepted, else False.
    """
    statistic, pvalue = scipy.stats.jarque_bera(r)
    return pvalue > level

def var_historic(r, levels=5):
    """
    Returns the historic value at risk at a specified level
    i.e returns the level such that "level" percent of returns
    fall below that number, and the (100-level) percent are above
    """
    if isinstance(r, pd.DataFrame):
        return r.aggregate(var_historic, levels=levels)
    elif isinstance(r, pd.Series):
        return -np.percentile(r, levels, axis=0)
    else:
        raise TypeError("Series or DataFrame expected")

def var_gaussian(r, levels=5, modified=False):
    """
    Returns the Parametric Gaussian VaR of a Series or DataFrame
    If modified is  True, then the modified VaR is returned
    using Cornish-Fisher modifications
    """
    # compute the Z score assuming the distribution is Gaussian
    z = norm.ppf(levels / 100)

    if modified:
        # calculate the Z score based on kurtosis and skewness
        s = skewness(r)
        k = kurtosis(r)
        z = (z +
             (z ** 2 + 1) * s / 6 +
             (z ** 3 - 3 * z) * (k - 3) / 24 +
             (2 * z ** 3 - 5 * z) * (s ** 2) / 36
             )

    return -(r.mean() + z * r.std(ddof=0))

def cvar_historic(r, levels=5):
    """
    Computes the conditional VaR or CVaR of the Series or DataFrame
    """
    if isinstance(r, pd.Series):
        is_beyond = r <= -var_historic(r, levels=levels)
        return -r[is_beyond].mean()
    elif isinstance(r, pd.DataFrame):
        return r.aggregate(cvar_historic, levels=levels)
    else:
        raise TypeError("Expected the input to be DataFrame or Series")


# Reference: https://stackabuse.com/gradient-descent-in-python-implementation-and-theory/

import numpy as np
import plotly.graph_objects as go

# User defined modules
from test_functions import available_functions

class GradientDescent:
    def __init__(self, objective_function, initial_point, precision=1e-6, max_iter=10000, rate=0.1, momentum=0.1):
        self.objective_fn = objective_function
        self.initial_point = np.array(initial_point)
        self.precision = precision
        self.max_iter = max_iter
        self.learning_rate = rate
        self.momentum = momentum

        self.step_size = 1
        self.iters = 0
        self.points_history = np.array(initial_point)
        self.fun_history = np.array(objective_function.calculate(initial_point))
        self.name = "Gradient Descent"

    def optimize(self):
        current_point = self.initial_point
        delta_pts = np.zeros(current_point.shape)  # change in points
        print("Starting point = ", np.round(self.initial_point, 2))
        while self.step_size > self.precision and self.iters < self.max_iter:
            # print("Iteration: ", self.iters)
            delta_pts = -self.learning_rate * self.objective_fn.gradient(current_point) + self.momentum * delta_pts
            current_point = current_point + delta_pts
            self.update_history(current_point)

            self.step_size = np.absolute(self.fun_history[-1] - self.fun_history[-2])
            self.iters += 1
        return {"minimum": self.fun_history[-1], "position": current_point, "path": self.optimization_path()}

    def update_history(self, new_point):
        self.points_history = np.vstack((self.points_history, new_point))
        self.fun_history = np.vstack((self.fun_history, self.objective_fn.calculate(new_point)))

    def print_output(self):
        minimum = np.round(self.fun_history[-1], 3)
        position = np.round(self.points_history[-1], 3)
        print(f'Predicted minimum = {minimum} at {position}')

    def optimization_path(self):
        return go.Scatter(x=self.points_history[:, 0], y=self.points_history[:, 1], mode="lines+markers")

def available_optimizers():
    return {"Gradient Descent": GradientDescent}

def optimize_function(point, fn_name: str, alg_name: str):
    test_fns = available_functions()
    selected_fn = test_fns[fn_name]
    optimizers = available_optimizers()
    selected_alg = optimizers[alg_name]
    optimizer_obj = selected_alg(selected_fn(), point)
    output = optimizer_obj.optimize()
    optimizer_obj.print_output()
    minimum = np.round(output["minimum"], 3)
    position = np.round(output["position"], 3)
    return minimum[0], (position[0], position[1]), output["path"]

if __name__ == "__main__":
    funs = available_functions()
    opt_algorithms = available_optimizers()
    for alg_name, algs in opt_algorithms.items():
        print(alg_name)
        for fn_name, test_fn in funs.items():
            print(fn_name)
            fun_object = test_fn()
            opt_obj = algs(fun_object, np.random.uniform(fun_object.lower_limit, fun_object.upper_limit, 2))
            _ = opt_obj.optimize()
            opt_obj.print_output()


import numpy as np

def probability_of_sum(total:int, dice1, dice2):

    """
    Brief:
    Basic probability - Dice cast
    Suppose a pair of fair 6-sided dice are thrown.
    What is the probability that the sum of the rolls is 6? (Answer as a simple fraction of integers)
    reference: https://statweb.stanford.edu/~susan/courses/s60/split/node65.html
    """

    n = dice1.shape[0]
    m = dice2.shape[0]

    comb = n * m
    count = 0

    for i in dice1:
        for j in dice2:
            sum = int(i + j)
            if sum == total:
                count += 1

    prob = count / comb

    return print("{:.2%}".format(prob))

# define the dice as a linear array of 1 to 6, all integers
dice1 = np.linspace(1,6,6,dtype=int)

# call the function above with the total for which we would like to calculate the probability with 2 dices
prob = probability_of_sum(6, dice1, dice1)


import numpy as np

# Relu
def relu(y) -> np.ndarray:
    return np.maximum(y, 0)

def relu_prime(y) -> np.ndarray:
    return y > 0

# Leaky relu
def leaky_relu(y) -> np.ndarray:
    return np.where(y > 0, y, y * 0.01)

def leaky_relu_prime(y) -> np.ndarray:
    return (y >= 0) + (y < 0)*0.01

# Linear
def linear(y) -> np.ndarray:
    return y

def linear_prime(y) -> np.ndarray:
    return 1

# Heavyside
def heaviside(y) -> np.ndarray:
    return 1 * (y > 0)

def heaviside_prime(y) -> np.ndarray:
    return 0

# Sigmoid
def sigmoid(y) -> np.ndarray:
    return 1 / (1 + np.exp(-y))

def sigmoid_prime(y) -> np.ndarray:
    return sigmoid(y) * (1 - sigmoid(y))

# Tanh
def tanh(y) -> np.ndarray:
    return np.tanh(y)

def tanh_prime(y) -> np.ndarray:
    return 1 - tanh(y)**2

# Arctan
def arctan(y) -> np.ndarray:
    return np.arctan(y)

def arctan_prime(y) -> np.ndarray:
    return 1 / (y**2 + 1)

# Softmax
def softmax(y) -> np.ndarray:
    e = np.exp(y - np.max(y))
    return e / e.sum(axis=0)

# TODO implement a solution to derive softmax : the result has not the same shape as the input
def softmax_prime(y) -> np.ndarray:
    s = y.reshape(-1, 1)
    return np.mean(np.diagflat(s) - np.dot(s, s.T))

def listToActivations(activations_list, architecture) -> list:
    activations_fn = []
    activations_prime = []
    for id, _ in enumerate(architecture):
        if id < len(activations_list):

            if activations_list[id] == 'relu':
                activations_fn.append(relu)
                activations_prime.append(relu_prime)

            elif activations_list[id] == 'leaky_relu':
                activations_fn.append(leaky_relu)
                activations_prime.append(leaky_relu_prime)

            elif activations_list[id] == 'linear':
                activations_fn.append(linear)
                activations_prime.append(linear_prime)

            elif activations_list[id] == 'heaviside':
                activations_fn.append(heaviside)
                activations_prime.append(heaviside_prime)

            elif activations_list[id] == 'sigmoid':
                activations_fn.append(sigmoid)
                activations_prime.append(sigmoid_prime)

            elif activations_list[id] == 'tanh':
                activations_fn.append(tanh)
                activations_prime.append(tanh_prime)

            elif activations_list[id] == 'arctan':
                activations_fn.append(arctan)
                activations_prime.append(arctan_prime)

            elif activations_list[id] == 'softmax':
                print(
                    'Error : transfert function `', activations_list[id], '` is not fully implemented.', sep='')
                exit(1)
                activations_fn.append(softmax)
                activations_prime.append(softmax_prime)

            else:
                print(
                    'Error : transfert function `', activations_list[id], '` does not exist.', sep='')
                exit(1)

        # If not defined, fallback function is relu
        else:
            activations_fn.append(relu)
            activations_prime.append(relu_prime)

    return activations_fn, activations_prime


# coding: utf-8

# # LASP REU Python Tutorial Day 2
#
# ## Imports
#
# Remember the huge stack of science libraries (or the list of built-in libraries)?
#
# So, how to get those?
#
# With imports:

# In[ ]:

from math import pi
tau = 2*pi
print("The real circular constant is", tau)
# read tauday.com if you don't believe me. ;)

# How to import other libraries, their functions or their values is very flexible.
#
# One can distinguish mainly 2 kinds:
# * One imports a function directly into the current `namespace`:
# ```python
# from math import sin
# ```
# * import the top module and access via the dot `.`:
# ```python
# import math
# print(math.sin(0.5))
# ```
#
# In both cases one can rename the object being imported.
#
# This is a very powerful feature of Python: You can do what you want in terms of how to call things up.

# In[ ]:

from math import sin as stupid_sine
print(stupid_sine(0.5))

# In[ ]:

import math as i_hate_math
print(i_hate_math.cos(-1))

# ## Function returns, packing and unpacking
#
# Functions can return values.
#
# (you saw this in the last exercise already):

# In[ ]:

from math import log10

def mylog(value):
    if value < 0:
        return "Logarithm not defined."
    else:
        return log10(value)

# In[ ]:

result = mylog(-1)
result

# In[ ]:

import math
math.log10(-1)

# Maybe you want to return more than one value?
#
# Python will automatically pack things up:

# In[ ]:

from math import log10

def mylog(value):
    if value < 0:
        return "Logarithm not defined."
    else:
        return value, log10(value)

# In[ ]:

result = mylog(3)
result

# In[ ]:

print(type(result))
len(result)

# Tuples are the immutable version of lists:

# In[ ]:

result[0] = 4

# In[ ]:

mylist = list(result)
mylist

# In[ ]:

mylist[0] = 5
mylist

# and "iterable" as well (meaning I can loop over them):

# In[ ]:

for item in result:
    print(item)

# In[ ]:

waves = [5000, 6000]

# In[ ]:

for wave in waves:
    print(wave)

# In[ ]:

d = {'a':5, 'b':6}
d

# In[ ]:

for key,val in d.items():
    print(key, val)

# Python will always automatically pack for you (as it did above with result), but will never
# automatically unpack for you:

# In[ ]:

val, res = mylog(3)
print(val)
res

# Because of the automatic packing, you can always have less than required variables, but never more:

# In[ ]:

a, b, c = mylog(17)

# In[ ]:

out = mylog(17)

# In[ ]:

type(out)

# In[ ]:

out[0]

# In[ ]:

out[1]

# ### Interlude: strings
#
# They are also iterables:

# In[ ]:

for char in "Han":  # Remember that I decide on the name of my temporary variable
    print(char)

# Strings have a lot of useful support functions (lingo: `methods`) inside them:

# In[ ]:

s = "Han shot first!"

# In[ ]:

get_ipython().magic('pinfo s.center')

# In[ ]:

s.split()   # by default, spaces are assumed to be the separator

# In[ ]:

s.split('s')  #  note that the separator can be anything, but it will be removed!

# Why are the methods already available when I did not store a string into a variable?

# In[ ]:

'{1}, I am your {0}.'.format('father', 'Luke')

# Note, I am using the `format` method even so I haven't stored that string anywhere.
#
# Now, even an empty string or a string with only a space has those methods, and one very useful is the 'join' method:

# In[ ]:

' '.join(["It's", 'a','trap!'])

# ### Back to functions: Default values
# Functions can have optional arguments that hold a default value:

# In[ ]:

def sub_reverser(alist, index=0):
    reversed_list = list(reversed(alist))
    return ''.join(reversed_list[index:])

# In[ ]:

''.join(list(reversed('astring')))

# In[ ]:

sub_reverser('astring')

# In[ ]:

sub_reverser('astring', 3)

# This is a very powerful design feature of Python as well: I only need to write one function, but can use it in very different ways, depending on my default arguments (also known as `keyword argument`).

# Ok, let's go to some more meaty science libraries.
#
# ## matplotlib gallery
#
# * Can't go into depth of matplotlib library here, very rich and powerful
# * Best way to learn: Go to their gallery page http://matplotlib.org/gallery.html

# In[ ]:

from IPython.display import IFrame
IFrame("http://matplotlib.org/gallery.html", width=800, height=350)

# ## numpy
#
# For any serious array/vector/matrix based math, you should use the numpy library.
#
# It is faster, because in contrast to lists, it insists on keeping every item the same data-type. This enables under the hood to create C-objects and pass them to the C or Fortran math libraries.
#
# These libraries are decade old standards with very well researched behaviour!
#
# Contrary to the above mentioned freedom for importing, there are some standards that you should just adapt if you don't want to confuse yourself, when searching for tutorials, blogs etc. to help you out.
#
# This standard imports are:

# In[ ]:

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

# Okay, let's do some math:

# In[ ]:

mylist = list(range(10))
mylist

# In[ ]:

arr = np.array(mylist)
arr

# Most import feature of numpy arrays is to support vector math, which lists can't do:

# In[ ]:

mylist / 3

# In[ ]:

arr / 3

# Lingo: `Lists` are Python lists, `arrays` are numpy arrays.
#
# Basic indexing works the same:

# In[ ]:

mylist[2:4]

# In[ ]:

arr[2:4]

# numpy has it's own range function: `arange()` (standing for array-range):

# In[ ]:

np.arange(10)

# In[ ]:

get_ipython().magic('pinfo np.arange')

# In[ ]:

arr = np.arange(1, 11, 2, dtype='float')  # works the same as range, but also type-able

# In[ ]:

arr.dtype

# Handy array creators:

# In[ ]:

np.ones(3)

# In[ ]:

np.zeros(5)

# Multi-dimensional:

# In[ ]:

np.ones((2,3))  # rows first, then columns

# Note that the `ones` function wants the dimensions as a tuple if more than 1, to not confuse things with other arguments.
#
# Here's 3D:

# In[ ]:

np.zeros((2,4,3))  # the depth dimension (how many 2D arrays) first!

# Lot's of methods inside the numpy array object:

# In[ ]:

arr.

# Most useful:

# In[ ]:

arr.mean()

# In[ ]:

arr.std()

# In[ ]:

arr.dot(arr)

# In[ ]:

np.ones((3,3)).diagonal()

# In[ ]:

arr.max()

# In[ ]:

arr.argmax()  # WHERE is the max?

# Lazy indexing one of most important functionality:

# In[ ]:

arr

# In[ ]:

arr < 3

# In[ ]:

arr[arr<3]

# 2D numpy (= matrices) have a lot of matrix features:

# In[ ]:

arr2d = np.random.randn(2,3)  # randn provides Gaussian-distributed random values.
arr2d

# In[ ]:

get_ipython().magic('matplotlib inline')

plt.hist(np.random.randn(1000), bins=100, color='green');

# In[ ]:

arr2d

# In[ ]:

arr2d.transpose()

# Absolutely impossible to cover `numpy` even half here, but it's one of the most important tools in Python.
#
# ## pandas
#
# Most important high-level data analysis library (in my POV).
#
# It uses `numpy` under the hood.
#
# So, my recommendation:
# 1. Learn the numpy basics, don't try to get it all in, hardly possible.
#  * For example the scipy quickstart tutorial
# 2. Learn Pandas first, and whenever they refer to an unknown numpy feature, look that up.
#
# Pandas has a nice 10 minutes intro here: http://pandas.pydata.org/pandas-docs/stable/10min.html
#
# Most important objects in `pandas` are Series and DataFrames

# In[ ]:

import pandas as pd

s = pd.Series(np.arange(10)*24.1)
s

# Important concept in `pandas` are the indexes. `pandas` always keeps the relationship between indices and data intact!
#
# series filtering works the same as for numpy arrays, as it's build on it:

# In[ ]:

s[s<100]

# DataFrames are the 2D version of Series, complete with column names.
#
# Another very enticing feature of `pandas` are its datetime abilities:

# In[ ]:

dates = pd.date_range('20130101', periods=10)

print(dates)
df = pd.DataFrame(np.random.randn(10,4), index=dates, columns=list('ABCD'))
df

# In[ ]:

df.loc['2013-01-01','A':'C']

# Coolest part is the automatic datetime and multi-graph plotting of DataFrames:

# In[ ]:

df.plot()

# It is using `matplotlib` under the hood, so you could do this yourself, but with much more code.

# In[ ]:

df.head()

# In[ ]:

df.tail(3)

# In[ ]:

df.loc['2013-01-4':'2013-01-09', 'B':'C']

# Above, using the `.loc` attribute of dataframes, this is the fully official way to select data in a dataframe in all possible ways. The first part will always filter the rows you want, the second part always on the columns.
#
# But if you only want to filter/select on your current index, one can put these conditions directly like an indexing choice into brackets `[]`:

# In[ ]:

df['2013-01-4':'2013-01-09']

# In[ ]:

df[df > 0]

# In[ ]:

df.describe()

# In real-life interactive data analysis, one often calculates new sets of values based on measured stuff.
#
# It's a breeze with pd.DataFrames:

# In[ ]:

df['E'] = df.A + df.B

# In[ ]:

df['my new column'] = df.C + df.D

# In[ ]:

df['my new column']

# In[ ]:

df.head()

# ### groupby
#
# Let's have a look at one last powerful feature: Grouping and per-group stats.
#
# First, we need to create something to group by:

# In[ ]:

import random

# In[ ]:

group = [random.choice('abc') for _ in range(df.shape[0])]
group

# In[ ]:

df['group'] = group
df

# In[ ]:

g = df.groupby('group')
g.size()

# In[ ]:

g.mean()

# Can you imagine how much code you would have to write to do this from scratch?
#
# Ok, exercise time!


import numpy as np
import scipy
import sympy
from numpy import linalg as lg
from numpy.linalg import solve
from numpy.linalg import eig
from scipy.integrate import quad

# Question 1
'''
A. Determinant = -21

B. Determinant = -21

'''

m1 = np.array([[3, 0, 3], [2, 3, 3], [0, 4, -1]])
print(m1)

det1 = np.linalg.det(m1)
print(det1) # correct

# Question 2
# Det = -159

# Question 3
'''
A.
Replace row 3 with k times row 3.

B.
The determinant is multiplied by k.
'''

# Question 4
m2 = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 1]])
det2 = np.linalg.det(m2)
print(det2) # correct

# Question 5
'''
A.
False, because the determinant of A can be computed by cofactor expansion across any row or down any column. Since the determinant of A is well defined, both of these cofactor expansions will be equal.

B.

False, because the determinant of a triangular matrix is the product of the entries along the main diagonal.
'''

# Question 6
'''
If two rows of A are interchanged to produce B, then det Upper B equals negative det A.
'''

# Question 7
'''
If a multiple of one row of A is added to another row to produce matrix B, then det Upper B equals det Upper A.
'''

# Question 8
m3 = sympy.Matrix([[1, 5, -6], [-1, -4, -5], [1, 4, 7]])
print(m3)
rref1 = m3.rref()
print(rref1)

m4 = np.array([[1, 5, -6], [-1, -4, -5], [1, 4, 7]])
det3 = np.linalg.det(m4)
print(det3) # correct, det = 2

# Question 9
# Switch the rows, det of original matrix = -10, det of changed matrix = 10

# Question 10
m5 = np.array([[-25, -4, -2], [-5, 12, -4], [0, -20, 6]])
det4 = np.linalg.det(m5)
print(det4)
# The matrix is invertible because the determinant of the matrix is not zero.

# Question 11
# formula

# Question 12
mat = np.array([[1,1,0], [3, 0, 5], [0, 1, -5]])
print(mat)
det8 = np.linalg.det(mat)
print(det8)

#Cramer's Rule
# Find A1b by replacing the first column with column b
mat2 = np.array([[2,1,0], [0, 0, 5], [3, 1, -5]])
print(mat2)
det9 = np.linalg.det(mat2)
print(det9)
print(det9/det8)

#Find A2b by replacing the second column with b
mat3 = np.array([[1, 2, 0], [3, 0, 5], [0, 3, -5]])
print(mat3)
det10 = np.linalg.det(mat3)
print(det10)
print(det10/det8)

#Find A3b by replacing the third column with b
mat4 = np.array([[1, 1, 2], [3, 0, 0], [0, 1, 3]])
print(mat4)
det11 = np.linalg.det(mat4)
print(det11)
print(det11/det8)

# Answers above are correct, but try again because I misread the print output

matr = np.array([[1,1,0], [5, 0, 4], [0, 1, -4]])
print(matr)
deter = np.linalg.det(matr)
print(deter)

# Find A1b by replacing first column with b
matr1 = np.array([[5, 1, 0], [0, 0, 4], [6, 1, -4]])
print(matr1)
deter1 = np.linalg.det(matr1)
print(deter1/deter)

# Find A2b by replacing second column with b
matr2 = np.array([[1, 5, 0], [5, 0, 4], [0, 6, -4]])
print(matr2)
deter2 = np.linalg.det(matr2)
print(deter2/deter)

# Find A3b by replacing third column with b
matr3 = np.array([[1, 1, 5], [5, 0, 0], [0, 1, 6]])
print(matr3)
deter3 = np.linalg.det(matr3)
print(deter3/deter)

# Question 13
# Compute the adjugate of the given matrix
matri = np.matrix([[2, 5, 4], [1, 0, 1], [3, 2, 2]])
print(matri)
# Hermitian transpose (not correct)
print(matri.getH())
# Det of matrix
determ = np.linalg.det(matri)
print(determ)

adj_matr = np.array([[-2, -2, 5], [1, -8, 2], [2, 11, -5]])
print(adj_matr * 1/determ) # Correct

# Question 14
m6 = np.array([[3, 7], [6, 2]])
print(m6)
det5 = np.linalg.det(m6)
print(det5) # correct
# The area of the parellelogram is the absolute value of the det. In this case = 36

# Question 15
# First find the area of the parellelogram
m7 = np.array([[-5, -5], [5, 10]])
det6 = np.linalg.det(m7)
print(det6) # -25

# next find the det of matrix A
m8 = np.array([[7, -8], [-2, 8]])
print(m8)
det7 = np.linalg.det(m8)
print(det7) # 40

# Finally, multiply the absolute value of the det of the first matrix (area of the parellelogram) by the det of the second matrix
# Answer = 1000

import numpy,from numpy,import scipy,from scipy,import sympy


# --------------
# Code starts here

import numpy as np

# Code starts here

# Adjacency matrix
adj_mat = np.array([[0,0,0,0,0,0,1/3,0],
                   [1/2,0,1/2,1/3,0,0,0,0],
                   [1/2,0,0,0,0,0,0,0],
                   [0,1,0,0,0,0,0,0],
                  [0,0,1/2,1/3,0,0,1/3,0],
                   [0,0,0,1/3,1/3,0,0,1/2],
                   [0,0,0,0,1/3,0,0,1/2],
                   [0,0,0,0,1/3,1,1/3,0]])

# Compute eigenvalues and eigencevectrs

eigenvalues , eigencevectors = np.linalg.eig(adj_mat)

print(eigenvalues)

print(eigencevectors)

# Eigen vector corresponding to 1

eigen_1 = abs(eigencevectors[:,0])

print(eigen_1)

eigen_1 = abs(eigencevectors[:,0])/np.linalg.norm(eigencevectors[:,0],1)

print(eigen_1)

# most important page

page = np.where(np.max(eigen_1)== eigen_1)[0][0] + 1
print(page)

# Code ends here

# --------------
# Code starts here

adj_mat
print(adj_mat)

# Initialize stationary vector I

init_I = np.array([1,0,0,0,0,0,0,0])
print(init_I)

a= np.dot(adj_mat , init_I)
print(a)

b = np.linalg.norm(init_I , 1)
print(b)

for i in range(10):
        init_I = np.dot(adj_mat , init_I)
        init_I /= np.linalg.norm(init_I , 1)

print(init_I)

# Perform iterations for power method

power_page = np.where(np.max(init_I)==init_I)[0][0] +1
print(power_page)

# Code ends here

# --------------
# Code starts here

# New Adjancency matrix
# New Adjancency matrix
new_adj_mat = np.array([[0,0,0,0,0,0,0,0],
                   [1/2,0,1/2,1/3,0,0,0,0],
                  [1/2,0,0,0,0,0,0,0],
                   [0,1,0,0,0,0,0,0],
                   [0,0,1/2,1/3,0,0,1/2,0],
                   [0,0,0,1/3,1/3,0,0,1/2],
                   [0,0,0,0,1/3,0,0,1/2],
                   [0,0,0,0,1/3,1,1/2,0]])

# Initialize stationary vector I

new_init_I = np.array([1,0,0,0,0,0,0,0])

print(new_init_I)

# Perform iterations for power method
for _ in range(10):
    new_init_I = np.dot(new_adj_mat, new_init_I)
    new_init_I /= np.linalg.norm(new_init_I, 1)

print(new_init_I)

power_page = np.where(np.max(new_init_I) == new_init_I)[0][0] + 1
print(power_page)

print(new_init_I)

# Perform iterations for power method

# Code ends here

# --------------
# Alpha value
alpha = 0.85

new_adj_mat
print(new_adj_mat)

len(new_adj_mat)

np.ones(new_adj_mat.shape)

G = (alpha*new_adj_mat) + (1-alpha)*(1 / len(new_adj_mat))*np.ones(new_adj_mat.shape)

print(G)

final_init_I = np.array([1 , 0 , 0,0,0,0,0,0])
print(final_init_I)

for _ in range(1000):
    final_init_I = np.dot(G, final_init_I)
    final_init_I /= np.linalg.norm(final_init_I, 1)

print(final_init_I)

power_page = np.where(np.max(final_init_I) == final_init_I)[0][0] + 1
print(power_page)

print(final_init_I)

# Code starts here

# Modified adjancency matrix

# Initialize stationary vector I

# Perform iterations for power method

# Code ends here


# https://docs.sympy.org/latest/tutorial/solvers.html

from sympy import *
x, y, z = symbols('x y z')
init_printing(use_unicode=False)
from myprint import *
import myprint

spprint(Eq(x, y))

spprint(solveset(Eq(x**2, 1), x))
spprint(solveset(Eq(x**2 - 1, 0), x))
spprint(solveset(x**2 - 1, x))

spprint(solveset(x**2 - x, x))
spprint(solveset(x - x, x, domain=S.Reals))
spprint(solveset(sin(x) - 1, x, domain=S.Reals))

spprint(solveset(exp(x), x))     # No solution exists
spprint(solveset(cos(x) - x, x))  # Not able to find solution

spprint(linsolve([x + y + z - 1, x + y + 2*z - 3 ], (x, y, z)))
spprint(linsolve(Matrix(([1, 1, 1, 1], [1, 1, 2, 3])), (x, y, z)))

M = Matrix(((1, 1, 1, 1), (1, 1, 2, 3)))
system = A, b = M[:, :-1], M[:, -1]
spprint(linsolve(system, x, y, z))

a, b, c, d = symbols('a, b, c, d', real=True)
spprint(nonlinsolve([a**2 + a, a - b], [a, b]))
spprint(nonlinsolve([x*y - 1, x - 2], x, y))
spprint(nonlinsolve([x**2 + 1, y**2 + 1], [x, y]))

from sympy import sqrt
system = [x**2 - 2*y**2 -2, x*y - 2]
vars = [x, y]
spprint(nonlinsolve(system, vars))

system = [exp(x) - sin(y), 1/y - 3]
spprint(nonlinsolve(system, vars))

spprint(nonlinsolve([x*y, x*y - x], [x, y]))

system = [a**2 + a*c, a - b]
spprint(nonlinsolve(system, [a, b]))

spprint(solve([x**2 - y**2/exp(x)], [x, y], dict=True))

spprint(solve([sin(x + y), cos(x - y)], [x, y]))

spprint(solveset(x**3 - 6*x**2 + 9*x, x))

spprint(roots(x**3 - 6*x**2 + 9*x, x))

spprint(solve(x*exp(x) - 1, x ))

# diff eq

f, g = symbols('f g', cls=Function)

spprint(f(x))

spprint(f(x).diff(x))

diffeq = Eq(f(x).diff(x, x) - 2*f(x).diff(x) + f(x), sin(x))

spprint(diffeq)

spprint(dsolve(diffeq, f(x)))

spprint(dsolve(f(x).diff(x)*(1 - sin(f(x))) - 1, f(x)))


# numerical differentiation

import numpy as np

def numerical_diff(f, x):
    """
    Returns the derivative of f at x.
    """
    h = 1e-4  # rule of thumb
    return (f(x + h) - f(x - h)) / (2 * h)  # central difference

def naive_numerical_diff1(f, x):
    """
    Simple implementation by mathematical definition.
    """
    h = 1e-50  # '1e-4 for h is too large. It should be inifinitesimal.'
    return (f(x + h) - f(x - h)) / (2 * h)  # central difference

def naive_numerical_diff2(f, x):
    """
    Simple implementation by mathematical definition.
    """
    h = 1e-4  # rule of thumb
    return (f(x + h) - f(x)) / h  # 'Central diff is tricky.'

def show(f, x, diff, diff_label, expected):
    """
    f: function to be differentiated
    x: calculate the derivative at this point
    diff: function to get the derivative
    diff_label: name of `diff`
    expected: mathematically expected value for f'(x)
    """

    actual = diff(f, x)
    epsilon = 1e-5
    message = ''
    if np.linalg.norm(actual - expected) < epsilon:
        message = 'correct'
    else:
        message = 'wrong'

    print('[{0}] diff at {1} = {2} =====> {3}'.format(
        diff_label, x, actual, message))

if __name__ == '__main__':
    print('f(x) = x')
    show(lambda x: x, 0, numerical_diff, 'numerical_diff', 1)
    show(lambda x: x, 0, naive_numerical_diff1, 'naive_numerical_diff1', 1)
    show(lambda x: x, 0, naive_numerical_diff2, 'naive_numerical_diff2', 1)
    print()
    print('f(x) = x ** 2')
    show(lambda x: x ** 2, 2, numerical_diff, 'numerical_diff', 4)
    show(lambda x: x ** 2, 2, naive_numerical_diff1, 'naive_numerical_diff1', 4)
    show(lambda x: x ** 2, 2, naive_numerical_diff2, 'naive_numerical_diff2', 4)
    print()
    print('f(x) = x ** 3')
    show(lambda x: x ** 3, 5, numerical_diff, 'numerical_diff', 75)
    show(lambda x: x ** 3, 5, naive_numerical_diff1, 'naive_numerical_diff1', 75)
    show(lambda x: x ** 3, 5, naive_numerical_diff2, 'naive_numerical_diff2', 75)


# compare various approximations of derivatives

import math
import numpy
import pylab

def func(x):
    """ the function to be plotted """

    f = numpy.sin(x)
    return f

def fprime(x):
    """ the analytic derivative of func(x) """

    fp = numpy.cos(x)
    return fp

def d1l(dx, fc, i):
    """ first-order, left-sided derivative at index i """
    D = (fc[i] - fc[i-1])/dx
    return D

def d1r(dx, fc, i):
    """ first-order, right-sided derivative at index i """
    D = (fc[i+1] - fc[i])/dx
    return D

def d2(dx, fc, i):
    """ second-order centered derivative at index i """
    D = (fc[i+1] - fc[i-1])/(2.0*dx)
    return D

def d4(dx, fc, i):
    """ fourth-order centered derivative at index i """
    D = -fc[i+2] + 8.0*fc[i+1] - 8.0*fc[i-1] + fc[i-2]
    D = D/(12.0*dx)
    return D

def line(x, slope, x0, y0):
    return y0 + slope*(x - x0)

xl = 0.0
xr = math.pi

# fine grid (to show exact function)
fine = numpy.linspace(xl, xr, 500)

# coarse grid (for differencing)
coarse = numpy.linspace(xl, xr, 10)

dx = coarse[1] - coarse[0]

# plot the fine gridded analytic function
f = func(fine)
pylab.plot(fine, f, color="0.5", lw=2)

# plot the discrete data
c = func(coarse)
pylab.scatter(coarse, c)

# get the derivative approximations
Dl = d1l(dx, c, 3)
Dr = d1r(dx, c, 3)
D2 = d2(dx, c, 3)
D4 = d4(dx, c, 3)

analytic = fprime(coarse[3])

# function and point values
x0 = coarse[3]
y0 = func(x0)

xplot = numpy.linspace(coarse[1], coarse[5], 50)

pylab.plot(xplot, line(xplot, Dl, x0, y0), color="r", label="left-sided first-order approx")
pylab.plot(xplot, line(xplot, Dr, x0, y0), color="b", label="right-sided first-order approx")
pylab.plot(xplot, line(xplot, D2, x0, y0), color="g", label="centered second-order approx")
pylab.plot(xplot, line(xplot, D4, x0, y0), color="k", label="centered fourth-order approx")
#pylab.plot(xplot, line(xplot, analytic, x0, y0), color="0.5", ls=":", label="analytic")

print "analytic:          ", analytic
print "left-sided O(dx):  ", Dl
print "right-sided O(dx): ", Dr
print "centered O(dx**2): ", D2
print "centered O(dx**4): ", D4

leg = pylab.legend(loc=2,labelspacing=0.1)
ltext = leg.get_texts()
pylab.setp(ltext, fontsize='small')
leg.draw_frame(0)

# axes run through 0
# http://matplotlib.org/examples/pylab_examples/spine_placement_demo.html
ax = pylab.gca()
ax.spines['left'].set_position('zero')
ax.spines['right'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['top'].set_color('none')
ax.spines['left'].set_smart_bounds(True)
ax.spines['bottom'].set_smart_bounds(True)
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')

pylab.xlim(0,2.0)

pylab.savefig("fprime.png")


from numpy import pi
import numpy as np

array1 = np.array([2, 3, 4])

print(array1, array1.shape, array1.dtype)

# The type of the array can also be explicitly specified at creation time

complexTypeArray = np.array([[1, 2], [3, 4]], dtype=complex)
print(complexTypeArray)

# The function `zeros` creates an array full of zeros,
# the function `ones` creates an array full of ones,
# and the function `empty` creates an array whose initial content is random
# and depends on the state of the memory. By default, the `dtype` of the created array is `float64`.
arrayWithZeros = np.zeros((3, 4))
print('Array created with zeros funtion', arrayWithZeros)

arrayWithOnes = np.ones((2, 3, 4), dtype=np.int16)
print('Array created with ones function', arrayWithOnes)

emptyArray = np.empty((2, 3))
print('Array created with empty function', emptyArray)

# To create sequences of numbers, NumPy provides a function analogous to range that returns arrays instead of lists.

# Array from 10 to 30 with a range 5
arrayRange = np.arange(10, 30, 5)
print('Array with range', arrayRange)
# it accepts float arguments
arrayRange1 = np.arange(0, 2, 0.3)
print('Array with range floating point numbers', arrayRange1)

# When `arange` is used with floating point arguments, it is generally not possible
# to predict the number of elements obtained, due to the `finite floating point precision`.
# For this reason, it is usually better to use the function `linspace` that receives as an argument the `number of elements` that we want,
# instead of the step

# 9 numbers from 0 to 2
arrayRangeWithLineSpace = np.linspace(0, 2, 9)
print('Arrange with line space for floating numbers', arrayRangeWithLineSpace)

# useful to evaluate function at lots of points
x = np.linspace(0, 2*pi, 5)
f = np.sin(x)

# print(x)

# If an array is too large to be printed, NumPy automatically
# skips the central part of the array and only prints the corners:
# print(np.arange(10000).reshape(100, 100))

# To `disable` this behaviour and force NumPy to print the entire array, you can change the printing options using set_printoptions.
# np.set_printoptions(threshold=10)

# Basic Operations

# Arithmetic operators on arrays apply elementwise. A new array is created and filled with the result
a1 = np.array([20, 30, 40, 50])
b1 = np.arange(4)
arraySub = a1-b1

print('Actual array a1', a1)
print('Actual array b1', b1)

print('Subtract an array', arraySub)

arrayMultiplied = b1**2
print('Multiplied array', arrayMultiplied)

arraySinOperation = 10*np.sin(a1)
print(arraySinOperation)

arrayWithBoolean = a1 < 35
print(arrayWithBoolean)

# Unlike in many matrix languages, the product operator `*` operates `elementwise` in NumPy arrays.
# The matrix product can be performed using the `@` operator (in python >=3.5) or the `dot` function or method
A = np.array([[1, 1], [0, 1]])
B = np.array([[2, 0], [3, 4]])

print(A)
print(B)
# elementwise product
print('* operator performs elementwise product', A*B)
# matrix product
print('Matrix multiplication using @', A@B)
# another matrix product
print('Matrix multiplication using dot function', A.dot(B))

# Some operations, such as `+=` and `*=`, act in place to `modify an existing array`
# rather than create a new one.
a2 = np.ones((2, 3), dtype=int)
b2 = np.random.random((2, 3))
a2 *= 3
print('Mutated array using *=', a2)

b2 += a2
print('Mutated array using +=', b2)

# b is not automatically converted to integer type
# a += b

# Following error is throws
# Traceback (most recent call last):
#   ...
# TypeError: Cannot cast ufunc add output from
# dtype('float64') to dtype('int64') with casting rule 'same_kind'

# When operating with arrays of different types,
# the type of the resulting array corresponds to the more general or precise one
# (a behavior known as upcasting).

# Many unary operations, such as computing the
#  sum of all the elements in the array, are implemented as methods of the ndarray class.
randomArray = np.random.random((2, 3))
print('\nSum of the array:\n', randomArray.sum())

print('\nMinimum element in a array:\n', randomArray.min())

print('\nMaximum element in a array:\n', randomArray.max())

# By default, these operations apply to the array as though it were a list of numbers,
# regardless of its shape. However,
# by specifying the `axis` parameter you can apply an operation along the specified axis of an array

b3 = np.arange(12).reshape(3, 4)

print('\n Array used to apply sum, min, cumsum:\n', b3)

# axis-0 means column, axis-1 means - row
# sum of each column
print('\nSum of elements in axis-0:\n', b3.sum(axis=0))

# min of each row
print('\nMimimum of each row:\n', b3.min(axis=1))

# cumulative sum along each row
print('\nCumilative sum along each row:\n',  b3.cumsum(axis=0))

# Indexing, Slicing and Iterating
# One-dimensional arrays can be indexed, sliced and iterated over, much like lists and other Python sequences.
a3 = np.arange(10)**3
print('\nArray a3:\n', a3)

print('\nAccessing Array with index:\n', a3[2])
print('\nArray slicinng:\n', a3[2:5])
# equivalent to a[0:6:2] = -1000; from start to position 6, exclusive, set every 2nd element to -1000
a3[:6:2] = -1000

print('\nAfter manipulating a3:\n', a3)

print('\nReversed a3:\n', a3[::-1])

# Multidimensional arrays can have 'one index per axis'. These indices are given in a tuple separated by commas

def funtion1(x, y):
    return 10*x+y

b4 = np.fromfunction(funtion1, (5, 4), dtype=int)
print('\nArray generated using fromfuntion:\n', b4)

print('\nAccessing multidimensional array with index:\n', b4[2, 3])

# each row in the second column
print('\nEach row in the second column', b4[0:5, 1])
# b[ : ,1] # equivalent to the previous example

# each column in the second and third row
print('\nEach column in the second and third row:\n', b4[1:3, :])

# a 3D array (two stacked 2D arrays)
c1 = np.array([[[0,  1,  2],
                [10, 12, 13]],
               [[100, 101, 102],
                [110, 112, 113]]])

print('\nShape of a 3D array:\n', c1.shape)

# The dots (...) represent as many colons as needed to produce a complete indexing tuple
# same as c[1,:,:] or c[1]), first row
print('\nSlicing 3D array using (...):\n', c1[1, ...])

# same as c[:,:,2]), second column
print('\nSlicing 3D array using (...):\n', c1[..., 2])

# Iterating over multidimensional arrays is done with respect to the first axis
print('\nIterating over multidimensional arrays is done with respect to the first axis:\n')
for row in b4:
    print(row)

# However, if one wants to perform an 'operation on each element' in the array,
# one can use the 'flat' attribute which is an iterator over all the elements of the array
# print('\nPrinting the array elements using flat attribute:\n')
# for element in b4.flat:
    # print(element)

# Shape manipulation
a4 = np.floor(10*np.random.random((3, 4)))
print('\nArray before manipulation:\n', a4)

# Note that the following three commands all return a modified array, but do not change the original array

# returns the array, flattened
print('\nFlattened array using ravel():\n', a4.ravel())

print('\nArray with modified shape:\n', a4.reshape(6, 2))

print('\nTransposed array:\n', a4.T)

# `ndarray.resize()` method modifies the array itself, If a dimension is given as `-1` in a reshaping operation,
# the other dimensions are automatically calculated

# Stacking together different arrays
a5 = np.floor(10*np.random.random((2, 2)))

print('\nArray-1 to stack:\n', a5)

b5 = np.floor(10*np.random.random((2, 2)))
print('\nArray-2 to stack:\n', b5)

print('\nVertical stack array-1 over array-2:\n', np.vstack((a5, b5)))

print('\nHorizontal stack array-1 before array-2:\n', np.hstack((a5, b5)))


# identity
# The identity tool returns an identity array. An identity array is a square matrix with all the main diagonal elements as 1
# and the rest as 0. The default type of elements is float.

# import numpy
# print numpy.identity(3) #3 is for  dimension 3 X 3

# #Output
# [[ 1.  0.  0.]
#  [ 0.  1.  0.]
#  [ 0.  0.  1.]]

# eye
# The eye tool returns a 2-D array with 1's as the diagonal and 0's elsewhere. The diagonal can be main, upper or lower depending on the
# optional parameter k. A positive k is for the upper diagonal, a negative k is for the lower, and a 0 k(default) is for the main diagonal.

# import numpy
# print numpy.eye(8, 7, k = 1)    # 8 X 7 Dimensional array with first upper diagonal 1.

# #Output
# [[ 0.  1.  0.  0.  0.  0.  0.]
#  [ 0.  0.  1.  0.  0.  0.  0.]
#  [ 0.  0.  0.  1.  0.  0.  0.]
#  [ 0.  0.  0.  0.  1.  0.  0.]
#  [ 0.  0.  0.  0.  0.  1.  0.]
#  [ 0.  0.  0.  0.  0.  0.  1.]
#  [ 0.  0.  0.  0.  0.  0.  0.]
#  [ 0.  0.  0.  0.  0.  0.  0.]]

# print numpy.eye(8, 7, k = -2)   # 8 X 7 Dimensional array with second lower diagonal 1.

# Task
# Your task is to print an array of size N X M with its main diagonal elements as 1's and 0's everywhere else.

# Input Format
# A single line containing the space separated values of N and M.
# N denotes the rows.
# M denotes the columns.

# Output Format
# Print the desired N X M array.

# Sample Input
# 3 3

# Sample Output

# [[ 1.  0.  0.]
#  [ 0.  1.  0.]
#  [ 0.  0.  1.]]

import numpy as np

N, M = tuple(map(int, input().split()))
print(str(np.eye(N, M)).replace("0", " 0").replace("1", " 1"))


#!/usr/bin/python

'''
Learning Machines
Taught by Patrick Hebron at NYU ITP

Activation function implementations.
'''

import numpy as np

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

def dsigmoid(x):
	'''sigmoid derivative function'''
	y = sigmoid( x )
	return y * ( 1.0 - y )

def tanh(x):
	'''tanh function'''
	return np.sinh( x ) / np.cosh( x )

def dtanh(x):
	'''tanh derivative function'''
	return 1.0 - np.power( tanh( x ), 2.0 )


# -*- coding: utf-8 -*-
"""
Routines for numerical integration

@author: Nicolas Guarin-Zapata
"""
from __future__ import division, print_function
import numpy as np
# Gauss points and weights
from scipy.special import roots_legendre

def trapz(fun, x0, x1, n):
    """Trapezoidal rule for integration

    Parameters
    ----------
    fun : callable
        Function to integrate.
    x0 : float
        Initial point for the integration interval.
    x1 : float
        End point for the integration interval.
    n : int
        Number of points to take in the interval.

    Returns
    -------
    inte : float
        Approximation of the integral

    """
    x = np.linspace(x0, x1, n)
    y = fun(x)
    dx = x[1] - x[0]
    inte = 0.5*dx*(y[0] + y[-1])
    for cont in range(1, n - 1):
        inte = inte + dx*y[cont]
    return inte

def simps(fun, x0, x1, n):
    """Simpson's rule for integration

    Parameters
    ----------
    fun : callable
        Function to integrate.
    x0 : float
        Initial point for the integration interval.
    x1 : float
        End point for the integration interval.
    n : int
        Number of points to take in the interval.

    Returns
    -------
    inte : float
        Approximation of the integral

    """
    # We need an odd number of points
    # in case of having an even one
    # we add one point more
    if n%2 == 0:
        n = n + 1
    x = np.linspace(x0, x1, n)
    y = fun(x)
    dx = x[1] - x[0]
    inte = 0
    for cont in range(1, n//2 + 1):
        inte = inte + dx/3 * (y[2*cont - 2] + 4*y[2*cont - 1] + y[2*cont])
    return inte

def gauss1d(fun, x0, x1, n):
    """Gauss quadrature in 1D

    Parameters
    ----------
    fun : callable
        Function to integrate.
    x0 : float
        Initial point for the integration interval.
    x1 : float
        End point for the integration interval.
    n : int
        Number of points to take in the interval.

    Returns
    -------
    inte : float
        Approximation of the integral

    """
    xi, wi = roots_legendre(n)
    inte = 0
    h = 0.5 * (x1 - x0)
    xm = 0.5 * (x0 + x1)
    for cont in range(n):
        inte = inte + h * fun(h * xi[cont] + xm) * wi[cont]
    return inte

def gauss2d(fun, x0, x1, y0, y1, nx, ny):
    """Gauss quadrature for a rectangle in 2D

    Parameters
    ----------
    fun : callable
        Function to integrate.
    x0 : float
        Initial point for the integration interval in x.
    x1 : float
        End point for the integration interval in x.
    y0 : float
        Initial point for the integration interval in y.
    y1 : float
        End point for the integration interval in y.
    nx : int
        Number of points to take in the interval in x.
    ny : int
        Number of points to take in the interval in y.

    Returns
    -------
    inte : float
        Approximation of the integral

    """
    xi, wi = roots_legendre(nx)
    yj, wj = roots_legendre(ny)
    inte = 0
    hx = 0.5 * (x1 - x0)
    hy = 0.5 * (y1 - y0)
    xm = 0.5 * (x0 + x1)
    ym = 0.5 * (y0 + y1)
    for cont_x in range(nx):
        for cont_y in range(ny):
            f = fun(hx * xi[cont_x] + xm, hy * yj[cont_y] + ym)
            inte = inte + hx* hy * f * wi[cont_x] * wj[cont_y]
    return inte


#1. Write a NumPy program to sort a given array of shape 2 along the first axis, last axis and on flattened array.
import numpy as np
a = np.array([[10,40],[30,20]])
print("Original array:")
print(a)
print("Sort the array along the first axis:")
print(np.sort(a, axis=0))
print("Sort the array along the last axis:")
print(np.sort(a))
print("Sort the flattened array:")
print(np.sort(a, axis=None))
#2. Write a NumPy program to create a structured array from given student name, height, class and their data types. Now sort the array on height.
import numpy as np
data_type = [('name', 'S15'), ('class', int), ('height', float)]
students_details = [('James', 5, 48.5), ('Nail', 6, 52.5),('Paul', 5, 42.10), ('Pit', 5, 40.11)]
# create a structured array
students = np.array(students_details, dtype=data_type)
print("Original array:")
print(students)
print("Sort by height")
print(np.sort(students, order='height'))
#3. Write a NumPy program to create a structured array from given student name, height, class and their data types. Now sort by class, then height if class are equal
import numpy as np
data_type = [('name', 'S15'), ('class', int), ('height', float)]
students_details = [('James', 5, 48.5), ('Nail', 6, 52.5),('Paul', 5, 42.10), ('Pit', 5, 40.11)]
# create a structured array
students = np.array(students_details, dtype=data_type)
print("Original array:")
print(students)
print("Sort by class, then height if class are equal:")
print(np.sort(students, order=['class', 'height']))
#4. Write a NumPy program to sort the student id with increasing height of the students from given students id and height. Print the integer indices that describes the sort order by multiple columns and the sorted data.
import numpy as np
student_id = np.array([1023, 5202, 6230, 1671, 1682, 5241, 4532])
student_height = np.array([40., 42., 45., 41., 38., 40., 42.0])
#Sort by studen_id then by student_height
indices = np.lexsort((student_id, student_height))
print("Sorted indices:")
print(indices)
print("Sorted data:")
for n in indices:
  print(student_id[n], student_height[n])
#5. Write a NumPy program to get the indices of the sorted elements of a given array.
import numpy as np
student_id = np.array([1023, 5202, 6230, 1671, 1682, 5241, 4532])
print("Original array:")
print(student_id)
i = np.argsort(student_id)
print("Indices of the sorted elements of a given array:")
print(i)
#6. Write a NumPy program to sort a given complex array using the real part first, then the imaginary part
import numpy as np
complex_num = [1 + 2j, 3 - 1j, 3 - 2j, 4 - 3j, 3 + 5j]
print("Original array:")
print(complex_num)
print("\nSorted a given complex array using the real part first, then the imaginary part.")
print(np.sort_complex(complex_num))
#7. Write a NumPy program to partition a given array in a specified position and move all the smaller elements values to the left of the partition, and the remaining values to the right, in arbitrary order (based on random choice).

import numpy as np
nums = np.array([70, 50, 20, 30, -11, 60, 50, 40])
print("Original array:")
print(nums)
print("\nAfter partitioning on 4 the position:")
print(np.partition(nums, 4))
#8. Write a NumPy program to sort the specified number of elements from beginning of a given array.
import numpy as np
nums =  np.random.rand(10)
print("Original array:")
print(nums)
print("\nSorted first 5 elements:")
print(nums[np.argpartition(nums,range(5))])

NumPy: Sorting and Searching Exercises & solutions.py