# firstorder.py - First order polynomial fitting

import sys
import numpy as np

def algsol(x, y):
    """
    Function to fit a straight line to data and estimate slope and
    intercept of the line and corresponding errors using algebraic solution.

    Parameters
    ----------
    x : ndarray
        X-axis data

    y : ndarray
        Y-axis data

    Returns
    -------
    ndarray
        slope, intercept, SDslope, SDintercept

    Reference
    ---------

    """
    # Number of input points
    N = x.shape[0]

    # Estimate intercept and slope of the straight line fitting
    Sxx = np.sum((x - np.mean(x))**2)
    Syy = np.sum((y - np.mean(y))**2)
    Sxy = np.sum((x - np.mean(x)) * (y - np.mean(y)))
    calcSlope = Sxy / Sxx
    calcIntercept = np.mean(y) - calcSlope * np.mean(x)
    Sy = np.sqrt((Syy - Sxx * calcSlope**2) / (N - 2))
    SDSlope = Sy / np.sqrt(Sxx)
    SDIntercept = Sy * np.sqrt(1 / (N - (np.sum(x)**2 / np.sum(x**2))))

    return np.array([[calcSlope, calcIntercept], [SDSlope, SDIntercept]])

def polyFit(x, y):
    """
    Function to fit a straight line to data and estimate slope and
    intercept of the line and corresponding errors using first order
    polynomial fitting.

    Parameters
    ----------
    x : ndarray
        X-axis data

    y : ndarray
        Y-axis data

    Returns
    -------
    ndarray
        slope, intercept, SDslope, SDintercept

    Reference
    ---------

    """
    # Number of input points
    N = x.shape[0]

    # Estimate slope and intercept of fitted line
    slope, intercept = np.polyfit(x, y, 1)

    # Calculate standard deviation of slope and intercept
    yhat = intercept + slope * x
    residual = y - yhat

    Sx2 = np.sum(x**2)
    Sxx = np.sum((x - np.mean(x))**2)
    Sy_x = np.sqrt(np.sum(residual**2) / (N -2))
    SDslope = Sy_x / np.sqrt(Sxx)
    SDintercept = Sy_x * np.sqrt(Sx2 / (N * Sxx))

    return np.array([[slope, intercept], [SDslope, SDintercept]])

def linalgFit(x, y):
    """
    Function to fit a straight line to data and estimate slope and
    intercept of the line and corresponding errors using linear algebra.

    Parameters
    ----------
    x : ndarray
        X-axis data

    y : ndarray
        Y-axis data

    Returns
    -------
    ndarray
        slope, intercept, SDslope, SDintercept

    Reference
    ---------

    """
    # Number of input points
    N = x.shape[0]

    Sx = np.sum(x)
    Sx2 = np.sum(x**2)
    Sy = np.sum(y)
    Sxy = np.sum(x * y)

    X = np.array([[N,Sx],[Sx,Sx2]])
    Y = np.array([[Sy], [Sxy]])

    intercept, slope = np.linalg.lstsq( X, Y )[0]

    # Calculate standard deviation of slope and intercept
    yhat = intercept + slope * x
    residual = y - yhat

    Sxx = np.sum((x - np.mean(x))**2)
    Sy_x = np.sqrt(np.sum(residual**2) / (N -2))
    SDslope = Sy_x / np.sqrt(Sxx)
    SDintercept = Sy_x * np.sqrt(Sx2 / (N * Sxx))

    return np.array([[slope, intercept], [SDslope, SDintercept]])

def MonteCarloWorker(indata):
    """
    Monte Carlo worker function for single processor and multicore processing

    Parameters
    ----------
    indata : ndarray
        input parameters for worker function

    Returns
    -------
    mcoeffs : tuple
        Fitted parameters
    """
    # Unpack input data
    ind, x, coeffs, SDnoise = indata

    print >> sys.stdout, '\n Monte Carlo iteration - ', ind + 1

    # Form polynomical equation based on number of coefficients
    lx = len(x)
    n_coeffs = len(coeffs)
    i, ymc = 0, np.zeros(lx)
    while i < n_coeffs:
        ymc += coeffs[i] * x**i
        i += 1

    # Add normal random noise to y value
    ymc += SDnoise * np.random.randn(lx)

    # Fit the noisy data using numpy polyfit
    mcCoeffs = np.polyfit(x, ymc, n_coeffs-1)

    # return a tuple of slope and intercept
    return mcCoeffs

def MonteCarloSim(x, coeffs, SDnoise = 1.0, niter = 1000, parallel = False):
    """
    Monte Carlo Simulation routine for linear fitting using normal random
    noise. Standard deviation of noise should be closer to the one calculated
    by algebraic method to get relatistic results.

    Parameters
    ----------
    x : ndarray
        X-axis data

    coeffs : ndarray
        Fitted coefficients

    SDnoise : float
        Standard deviation of random noise

    niter : int
        Number of Monte Carlo iterations. Default is 1000.

    parallel : bool
        Do parallel processing? Default is False.

    Returns
    -------
    MCResults : ndarray
        Monte Carlo result
    """
    # Populate input data list
    idata = []
    ind = range(niter)
    idata = map(lambda i: [i, x, coeffs, SDnoise], ind)

    # Import multiprocessing module
    try:
        import multiprocessing as mp
    except:
        n_cpus = 1
    else:
        n_cpus = mp.cpu_count()

    # Conditional loop for single and multicore processing
    if n_cpus == 1 or parallel == False:
        results = map(MonteCarloWorker, idata)
    else:
        pool = mp.Pool( n_cpus )
        results = pool.map(MonteCarloWorker, idata)

    # Convert result array to numpy num_coeffxniter array. The first
    # row is slope values and second row is intercept values
    MCResults = np.array(results).T

    # Output mean value of slope and intercept as well as
    # standard deviation values
    return MCResults

def MonteCarlo(x, y, SDnoise = 1.0, niter = 1000, parallel = False):
    """
    Function to run Monte Carlo to determine mean slope and intercept.
    Standard deviation of slope and intercept is also calculated.

    Parameters
    ----------
    x : ndarray
        X-axis data

    y : ndarray
        Y-axis data

    SDnoise : float
        standard deviation of noise

    niter : int
        number of Monte Carlo iterations

    parallel : bool
        run in parallel mode? Default if False

    Returns
    -------
    ndarray
        Mean slope and intercept and SD of slope and intercept

    References
    ----------

    """
    # Estimate slope and intercept of fitted line
    coeffs = np.polyfit(x, y, 1)

    # Input coefficient array is inverted for ease of processing
    results = MonteCarloSim(x, coeffs[-1::-1], SDnoise, niter, parallel)

    return np.array([[np.mean(results[0,:]), np.mean(results[1,:])], [np.std(results[0,:]), np.std(results[1,:])]])

def BootStrapWorker(indata):
    """
    Bootstrap worker function for single processor and multicore processing

    Parameters
    ----------
    indata : ndarray
        input parameters for worker function

    Returns
    -------
    mcCoeffs : tuple
        Fitted parameters
    """
    # Unpack input values
    i, x, y, coeffs, resample = indata

    print >> sys.stdout, '\n Bootstrap iteration - ', str(i+1)

    lx = len(x)
    n_coeffs = len(coeffs)
    resamplex, resampley = np.copy(x), np.copy(y)
    if resample == 'case':
        # Case resampling - replacement of elements (duplicates allowed)
        j = 0
        while j < lx - 2:
            if np.random.rand() > 0.5:
                resamplex[j] = x[j+1]
                resampley[j] = y[j+1]
                j += 1

    elif resample == 'residual':
        # Calculate residuals
        i, yhat = 0, np.zeros(lx)
        while i < n_coeffs:
            yhat += coeffs[i] * x**i
            i += 1
        residual = y - yhat

        # Residual resampling by using residual of model and actual values.
        ind = np.arange(lx)
        resampley = map(lambda i: yhat[i] + np.random.choice(residual), ind)

    elif resample == 'wild':
        # Calculate residuals
        i, yhat = 0, np.zeros(lx)
        while i < n_coeffs:
            yhat += coeffs[i] * x**i
            i += 1
        residual = y - yhat

        # Wild resampling by using residual of model and actual values
        # and multiplied by normal distributed noise
        ind = np.arange(lx)
        resampley = map(lambda i: yhat[i] + np.random.choice(residual) * np.random.randn(), ind)

    else:
        print >> sys.stdout, 'Error: Only Case, Residual and Wild bootstrap resampling methods allowed. Exiting.'
        sys.exit(-1)

    # Fit the noisy data using numpy polyfit
    mcCoeffs = np.polyfit(resamplex, resampley, n_coeffs-1)

    # return a tuple of slope and intercept
    return mcCoeffs

def BootStrapSim(x, y, coeffs, resample = 'residual', numtrials = 1000, parallel = False):
    """
    Bootstrap Simulation routine for linear fitting.

    Parameters
    ----------
    x : ndarray
        X-axis data

    y: ndarray
        Y-axis data

    coeffs : ndarray
        Fitted coefficients

    resample : string
        Resampling method. Default is residual.

    numtrials : int
        Number of bootstrap trials. Default is 1000.

    parallel : bool
        Do parallel processing? Default is False.

    Returns
    -------
    BSResults : ndarray
        Monte Carlo result
    """
    # Populate input data list
    idata = []
    ind = range(numtrials)
    idata = map(lambda i: [i, x, y, coeffs, resample], ind)

    # Import multiprocessing module
    try:
        import multiprocessing as mp
    except:
        n_cpus = 1
    else:
        n_cpus = mp.cpu_count()

    # Conditional loop for single and multicore processing
    if n_cpus == 1 or parallel == False:
        results = map(BootStrapWorker, idata)
    else:
        pool = mp.Pool( n_cpus )
        results = pool.map(BootStrapWorker, idata)

    # Create a numpy num_coeffs x numtrials array, with each row
    # correspond to coeff. values in all the trials
    BSResults = np.array(results).T

    return BSResults

def BootStrap(x, y, resample = 'residual', numtrials = 1000, parallel = False):
    """
    Function to run Bootstrapping to determine mean slope and intercept.
    Standard deviation of slope and intercept is also calculated.

    Parameters
    ----------
    x : ndarray
        X-axis data

    y : ndarray
        Y-axis data

    resample : string
        Resampling method. Default is residual.

    nimtrials : int
        Number of bootstrapping trials

    parallel : bool
        run in parallel mode? Default if False

    Returns
    -------
    ndarray
        Mean slope and intercept and SD of slope and intercept

    References
    ----------

    """
    # Estimate slope and intercept of fitted line
    coeffs = np.polyfit(x, y, 1)

    # Input coefficient array is inverted for ease of processing
    results = BootStrapSim(x, y, coeffs[-1::-1], resample, numtrials, parallel)

    return np.array([[np.mean(results[0,:]), np.mean(results[1,:]), np.std(results[0,:]), np.std(results[1,:])]])


from sympy import isprime

def primorial_number_series(n):
    primorial_numbers = [1]  # by definition, primorial(0) = 1

    i = 2  # starting the check for prime numbers from 2
    while len(primorial_numbers) != n:  # the loop runs until we have length of the sequence equal to our requirement
        if isprime(i):  # checks whether a number is prime or not
            primorial_numbers.append(i * primorial_numbers[len(primorial_numbers) - 1])  # the new element in the sequence is obtained by multiplying the current prime number by the product of all previous prime numbers

        i += 1

    return primorial_numbers

if __name__ == "__main__":
    n = int(input("Enter the number of terms of the sequence you want to see: "))

    for i, j in enumerate(primorial_number_series(n)):  # iterating over the sequence of primorial numbers
        print("primorial({}) = {}".format(i, j))


'''
Mloc_TUK computes Tukey's M-estimate of
location, i.e.,

mu_hat = arg min_mu SUM_i rho_TUK(y_i - mu)

   INPUTS:
           y: real valued data vector of size N x 1
           c: tuning constant c>=0 . default = 4.685
           max_iters: Number of iterations. default = 1000
           tol_err: convergence error tolerance. default = 1e-5

   OUTPUT:
           mu_hat: Tukey's M-estimate of location
'''
import numpy as np

from robustsp.AuxiliaryFunctions.madn import madn
from robustsp.AuxiliaryFunctions.wtuk import wtuk

def MLocTUK(y,c=4.685, max_iters = 1000, tol_err = 1e-5):
    y = np.asarray(y) # ensure that y is a ndarray

    # previously computed scale estimate
    sigma_0 = madn(y)

    # initial robust location estimate
    mu_n = np.median(y);
    # computes tukey weights
    wtuk = lambda absx,cl: np.square(1-np.square(absx/cl)) * (absx<=cl)

    for n in range(max_iters+1):
        w_n = wtuk(np.absolute(y-mu_n)/sigma_0,c) # compute weights
        mu_n_plus1 = np.sum(w_n*y)/(np.sum(w_n)) # compute weighted average
        if np.absolute(mu_n_plus1-mu_n)/sigma_0 > tol_err: # breaking condition
            mu_n = mu_n_plus1 # update estimate of mean
            n = n+1 # increment iteration counter
        else:
            break

    return mu_n # final estimate


# Working on 2D Tensors basics -types, shape, indexing, slicing and operations

#Import Libraries
import numpy as np
import torch
import pandas as pd
import matplotlib.pyplot as plt

## Types and Shapes

twoD_list=[[11,12,13],[21,22,23],[31,32,33]]
twoD_tensor=torch.tensor(twoD_list)
with open('Lab1_Tensors_2D_Results.txt','a') as f:
    print('2D List <--> 2D Tensor: \n',file=f)
    print(twoD_tensor,file=f)
    print('\n',file=f)

with open('Lab1_Tensors_2D_Results.txt','a') as f:
    print('Basic elements: \n',file=f)
    print('The dimension of 2D tensor: ',twoD_tensor.ndimension(),file=f)
    print('The shape of 2D tensor: ',twoD_tensor.shape,file=f)
    print('The size of 2D tensor: ',twoD_tensor.size(),file=f)
    print('The no. of elements of 2D tensor: ',twoD_tensor.numel(),file=f)
    print('\n',file=f)

## Numpy <--> Tensor, Pandas DF --> Tensor

twoD_numpy=twoD_tensor.numpy()
new_twoD_tensor=torch.from_numpy(twoD_numpy)

with open('Lab1_Tensors_2D_Results.txt','a') as f:
    print('Numpy --> Tensor: \n',file=f)
    print('The numpy after converting:\n',twoD_numpy,file=f)
    print('The type of numpy: ',twoD_numpy.dtype,file=f)
    print('-------------------------------------------',file=f)
    print('The tensor from numpy:\n',new_twoD_tensor,file=f)
    print('The type of new tensor from numpy: ',new_twoD_tensor.dtype,file=f)
    print('\n',file=f)

df=pd.DataFrame({'a':[11,21,31],'b':[12,22,32]})
new_df_tensor=torch.from_numpy(df.values)
with open('Lab1_Tensors_2D_Results.txt','a') as f:
    print('Pandas DF --> Tensor: \n',file=f)
    print('Pandas to numpy:\n',df.values,file=f)
    print('Type before conversion: ',df.values.dtype,file=f)
    print('-------------------------------------------',file=f)
    print('The tensor after converting:\n',new_df_tensor,file=f)
    print('The type of new tensor after converting: ',new_df_tensor.dtype,file=f)
    print('\n',file=f)

## Indexing and Slicing
tensor_example=torch.tensor(twoD_list)
with open('Lab1_Tensors_2D_Results.txt','a') as f:
    print('Indexing: \n',file=f)
    print('Value on 2nd row and 3rd column: ',tensor_example[1,2],file=f)
    print('Value on 2nd row and 3rd column: ',tensor_example[1][2],file=f)
    print('-------------------------------------------',file=f)
    print('Indexing and Slicing: \n',file=f)
    print('Value on 1st row and first two columns: ',tensor_example[0,0:2],file=f)
    print('Value on 1st row and first two columns: ',tensor_example[0][0:2],file=f)
    print('\n',file=f)

## Operations - Add, Scalar Multiplication, Element-wise Product, Matrix Mulitplication

X=torch.tensor([[1,0],[0,1]])
Y=torch.tensor([[2,1],[1,2]])

XYadd=X+Y # Addition
Z=2*Y #Multilpcation
XY_times=X*Y #Element wise product

#If X and Y are not same dimension matrix then X*Y is not necessarily same as Y*X
X_new=torch.tensor([[0,1,1],[1,0,1]]) # 2 x 3 matrix
Y_new=torch.tensor([[1,1],[1,1],[-1,1]]) # 3 x 2 matrix
XY_mm=torch.mm(X_new,Y_new) # 2 x 2 result of product

with open('Lab1_Tensors_2D_Results.txt','a') as f:
    print('Operations: \n',file=f)
    print('Adding X and Y gives:\n',XYadd,file=f)
    print('Scalar Mutiplication:\n',Z,file=f)
    print('Element Wise Product:\n',XY_times,file=f)
    print('Matrix Multiplication Xnew,Ynew:\n',XY_mm,file=f)
    print('\n',file=f)

Deep Neural Networks with PyTorch/Module1/Lab1_Tensors_2D.py


# Implement Bayes Classifier here!
# region Packages
#This Region contains all imported packages that is used inside the code.
import numpy as np
import sklearn
from sklearn.metrics import f1_score
import math
# endregion

def classPartition(trainDataSet,trainLabels):
    '''
    :param trainDataSet:
    :param trainLabels:
    :return: ClassPartitionSet-A Dict having data-elements of a particular class grouped together.
    Author: KD
    Details: This Code Partitions the Train Data as per Class Labels
    '''
    ClassPartitionSet={}
    for i in range(len(trainDataSet)):
        dataElement=list(trainDataSet[i])
        dataLabel=trainLabels[i]
        #if(dataLabel!=3):
        if(dataLabel not in ClassPartitionSet):
            ClassPartitionSet[dataLabel]=[]
        ClassPartitionSet[dataLabel].append(dataElement)
    return ClassPartitionSet

def meanOfClass(featureValues):
    '''
    :param featureValues:
    :return:Mean of Feature Values
    Author: KD
    Details: This Code Calculates the mean of Feature Values.
    '''
    return np.mean(featureValues)

def stdevOfClass(featureValues):
    '''
    :param featureValues:
    :return:Standard Deviation of Feature Values
    Author: KD
    Details: This Code Calculates the Standard Deviation of Feature Values.
    '''
    return math.sqrt(np.var(featureValues))

def calMeanVar(featureSet):
    '''
    :param  featureSet:
    :return: meanVarSet
    Author: KD
    Details: This Code Calculates Mean and Variance for each feature of the feature set.
    '''
    meanVarSet = [(meanOfClass(attribute), stdevOfClass(attribute)) for attribute in zip(*featureSet)]
    return meanVarSet

def calculateGaussianDensity(x, mean, stdev):
    '''
    :param x:
    :param mean:
    :param stdev:
    :return:This code returns probability density f(x)~N(mean,stdev) for each x from test data set.
    Author: KD
    '''
    a=math.pow(x - mean, 2)
    b=math.pow(stdev, 2)
    b=2*b
    c=-(a/b)
    d=np.exp(c)
    exponent = np.exp(-(math.pow(x - mean, 2) / (2 * math.pow(stdev, 2))))
    prob=(1 / (math.sqrt(2 * math.pi) * stdev)) * exponent
    return prob

def calculateClassConditionalDensities(meanStdSet, testInputVector):
    '''
    :param meanStdSet:
    :param testInputVector:
    :return:classConditionalProb
    Author: KD
    Details: This code calculates the class conditional densities of input vector for each class.
    '''
    classConditionalProb = {}
    for classlabel, classmeanStd in meanStdSet.items():
        #classConditionalProb[classlabel] = 1
        for i in range(len(classmeanStd)):
            mean, stdev = classmeanStd[i]
            if (stdev != 0):
                x = testInputVector[i]
                if (classlabel not in classConditionalProb):
                    classConditionalProb[classlabel]=calculateGaussianDensity(x, mean, stdev)
                else:
                    classConditionalProb[classlabel] *= calculateGaussianDensity(x, mean, stdev)
            else:
                continue
    return classConditionalProb

def bayesClassifier(trainData, trainLabels, testData,testLabels):
    '''
        :param dataMatrix:
        :param labelMatrix:
        :return: Accuracy of the classifier
        Author: KD
        Details: This is the main BayesClassifier function.
        '''
    # region handle older versions of sklearn
    if int((sklearn.__version__).split(".")[1]) < 18:
        from sklearn.cross_validation import train_test_split
    # otherwise we're using at lease version 0.18
    else:
        from sklearn.model_selection import train_test_split
    # endregion

    # region Sklearn Package  Bayes
    '''
    from sklearn.naive_bayes import GaussianNB
    print("Executing Sklearn Bayes Classifier")
    gnb = GaussianNB()
    y_pred = gnb.fit(trainData, trainLabels).predict(testData)
    Total = len(testData)
    predicted = Total - (testLabels != y_pred).sum()
    accuracyOfMyCode = (predicted / Total) * 100.0
    f1_score_macro = f1_score(testLabels, y_pred, average='macro')
    f1_score_micro = f1_score(testLabels, y_pred, average='micro')
    '''
    # endregion

    # region Custom  Bayes Classifier
    print("Executing Custom Bayes Classifier")
    classPartitionSet = classPartition(trainData, trainLabels)
    priorProbSet = {}
    for classValue, features in classPartitionSet.items():
        priorProbSet[classValue] = len(features) / len(trainData)

    # region Other Two Data Sets
    meanStdSet = {}
    for classValue, features in classPartitionSet.items():
        meanStdSet[classValue] = calMeanVar(features)

    predictions = []
    accuracy = 0
    for i in range(len(testData)):
        testVector = testData[i]
        probabilities = calculateClassConditionalDensities(meanStdSet, testVector)
        posteriorProb={}
        for key in probabilities:
            posteriorProb[key]=priorProbSet[key]*probabilities[key]
        predictedLabel = None
        PredictionProb = -1
        for classValue, probability in posteriorProb.items():
            if predictedLabel is None or probability > PredictionProb:
                PredictionProb = probability
                predictedLabel = classValue

        predictions.append(predictedLabel)
    # endregion

    for x in range(len(testData)):
        if testLabels[x] == predictions[x]:
            accuracy += 1
    accuracyOfMyCode = (accuracy / len(testData)) * 100.0
    f1_score_macro = f1_score(testLabels, predictions, average='macro')
    f1_score_micro = f1_score(testLabels, predictions, average='micro')
    # endregion

    return accuracyOfMyCode, f1_score_macro, f1_score_micro


"""
    Simple Implementation of Logistic Regression (LR) for binary classification.
    Label (class): y \in {+1, -1}.
    Model: p(y = +1 | x) = g(w'x),
    where g is a logistic function, w' means w transpose.
"""

import numpy as np
import scipy.optimize

def logistic(z):
    """ Logistic (sigmoid) function."""

    return 1.0/(1.0 + np.exp(-z))

def predict(w,x):
    """ If p(y = +1 | x) > 0.5, then class y = +1. Otherwise y = -1."""

    return logistic(np.dot(w,x)) > 0.5 or -1

"""
    Training phase: MLE + L2-regularized log-likelihood.
    To maximize the log-likelihood, we use gradient ascent for simplicity.
"""
def log_likelihood(X, Y, w, C=0.1):
    """
        Log likelihood function with regularization parameter C >= 0 (default: C = 0.1)
        See README for more details.
    """

    return np.sum(np.log(logistic(Y*np.dot(X,w))) - C/2*np.dot(w,w))

def log_likelihood_grad(X, Y, w, C=0.1):
    """ Compute the gradient of log-likelihood function above."""

    # d is dimensionality of a sample.
    d = len(w)
    # N is #training samples.
    N = len(X)
    s = np.zeros(d)

    for i in range(N):
        s += Y[i] * X[i] * logistic(-Y[i] * np.dot(X[i], w))

    s -= C*w
    return s

def grad_num(X, Y, w, f, eps=0.00001):
    """ Compute gradient numerically. Just for testing."""

    d = len(w)
    ident = np.identity(d)
    g = np.zeros(d)

    for i in range(d):
        g[i] += f(X,Y,w+eps*ident[i])
        g[i] -= f(X,Y,w-eps*ident[i])
        g[i] /= 2*eps
    return g

def train(X, Y, C=0.1):
    """
        Training phase by using BFGS algorithm to minimize -log-likelihood function.
    """
    def f(w):
        return -log_likelihood(X, Y, w, C)
    def fprime(w):
        return -log_likelihood_grad(X, Y, w, C)

    d = X.shape[1]
    initial_guess = np.zeros(d)

    return scipy.optimize.fmin_bfgs(f, initial_guess, fprime, disp=False)

def accuracy(X,Y,w):
    """
        First, evaluate the classifier on training data.
    """
    n_correct = 0
    for i in range(len(X)):
        if predict(w, X[i]) == Y[i]:
            n_correct += 1
    return n_correct * 1.0 / len(X)

# Splitting data in ith fold.
def fold(arr, K, i):
    """
        Split origin data to training data + validation data.
        Return: Tuple (hold-out samples, training samples).
    """
    N = len(arr)
    size = np.ceil(1.0 * N / K)
    arange = np.arange(N)
    holdout = np.logical_and(i*size <= arange, arange < (i+1)*size)
    rest = np.logical_not(holdout)
    return arr[holdout], arr[rest]

def kfold(arr, K):
    """ K-fold training data splitting."""
    return [fold(arr, K, i) for i in range(K)]

def avg_accuracy(all_x, all_y, C):
    """
        Calculate the CV score for each setup (C).
    """
    s = 0
    # nfolds is number of folds.
    nfolds = len(all_x)
    for i in range(nfolds):
        x_holdout, x_rest = all_x[i]
        y_holdout, y_rest = all_y[i]

        # Training phase to get w
        w = train(x_rest, y_rest, C)

        # Evaluate with hold-out samples
        s += accuracy(x_holdout, y_holdout, w)
    return s * 1.0 / nfolds

def train_C(X, Y, K=10):
    """ Using k-fold CV to optimize C."""
    # C candidates
    all_C = np.arange(0, 1, 0.1)
    all_X = kfold(X,K)
    all_Y = kfold(Y,K)
    all_acc = np.array([avg_accuracy(all_X, all_Y, C) for C in all_C])
    return all_C[all_acc.argmax()]

def read_data(filename, sep=",", aFilter=int):
    """
        Data format(ith line): label_i,x_i1,x_i2,...x_id
    """
    def split_line(line):
        return line.split(sep)
    def process_line(line):
        return map(aFilter, split_line(line))

    with open(filename) as f:
        lines = map(process_line, f.readlines())
        # adding x[0] = 1 for all rows
        X = np.array([ [1]+line[1:] for line in lines ])
        # the first column is class label of training data
        Y = np.array([line[0] or -1 for line in lines])
    return X, Y

def main(training_file='SPECT.train.txt', test_file='SPECT.test.txt'):
    # Read data
    X_train, Y_train = read_data(training_file)

    # Training phase
    optC = train_C(X_train, Y_train)
    print "Optimized C was", optC
    w = train(X_train, Y_train, optC)
    print 'w was', w

    # Testing phase
    X_test, Y_test = read_data(test_file)
    print 'Accuracy was', accuracy(X_test, Y_test, w)

if __name__ == '__main__':
    main()


# -*- coding: utf-8 -*-
"""
Created on Fri Oct 19 21:44:03 2018

@author: Tim

This solves the 1D Poisson equation by finite differences, assuming a constant
dielectric constant. A tridiagonal matrix is set up and solved.Con

Inputs: number of mesh points, left and right voltage boundary conditions,
dielectric constant.
"""

import numpy as np, time
import matplotlib
import matplotlib.pyplot as plt  # allows to make matlab style plots!

def thomasSolve(matrix, rhs):

    x = np.zeros(num_elements+2)  # array for the solution

   # Forward substitution
    for i in range(2, matrix.num_elements + 1): # recall range uses [ ) ]
        cdiag_ratio = matrix.lower_diag[i-1]/matrix.main_diag[i-1]
        matrix.main_diag[i] -= cdiag_ratio * matrix.upper_diag[i-1]
        rhs[i] -= cdiag_ratio * rhs[i-1]

    # Backward substitution
    x[num_elements] = rhs[num_elements]/matrix.main_diag[num_elements] #lin eqn. corresponding to last row
    for i in range(num_elements, 1, -1):  # 3rd argument is the step (iterate down)
        x[i-1] = (rhs[i-1] - x[i]*matrix.upper_diag[i-1])/matrix.main_diag[i-1]

    return x

class Poisson_matrix():

    def __init__(self, epsilon, num_elements):
        self.epsilon = epsilon  # save epsilon as attribute of the class
        self.num_elements = num_elements
        self.main_diag = -2*epsilon*np.ones(num_elements+1)
        self.upper_diag = epsilon*np.ones(num_elements)
        self.lower_diag = self.upper_diag  #poisson matrix is symmetric

# System setup

#num_elements =  int(input("Enter number of mesh points "))
#V_left_BC = float(input("Voltage at the left boundary "))
#V_right_BC = float(input("Voltage at the right boundary "))
#epsilon = float(input("Relative dielectric constant "))

num_elements = 99
V_left_BC = 0
V_right_BC = 0.9
epsilon = 3.8

start = time.time()

poisson_mat = Poisson_matrix(epsilon, num_elements)  # initialize object

# setup right hand side--> for now just hard code in a dipole, later will
# read the charges from file
rhs =  np.zeros(num_elements+1)  # make it +1 b/c I want to index from 1...
rhs[24] = 0.681
rhs[25] = -0.681

# apply boundary conditions
rhs[1] -= epsilon*V_left_BC
rhs[num_elements] -= epsilon*V_right_BC

V = thomasSolve(poisson_mat, rhs)

# add the boundary values (which didn't need to be solved for)
V[0] = V_left_BC
V[num_elements+1] = V_right_BC

end = time.time()
print(V)
print(f"CPU time: {end - start}")
# C++ version of this with 9999 elements runs in ~0.0032 sec

# make plot
fig, ax = plt.subplots()  # make a figure
ax.plot(V)
ax.set(xlabel = 'Position', ylabel='Voltage (V)', title = 'Poisson solver result')
ax.grid()  # add a grid
fig.savefig("poisson_result.jpg")
plt.show()


"""

Student Name: Hannah Richstein
Professor Name: Dr. Frinchaboy
Class: PHYS 50733
HW 3, Prob 2a, 2b
Last edited: 12 February 2017

Overview:
---------
This program implements two functions using different equation forms to solve
for the roots of the quadratic equation

Input:
------
The coefficients of the quadratic equation

Output:
-------
The two roots, as found by each function

Program Limitations:
--------------------
Machine precision

"""
from numpy import sqrt, array, sort

a = float(input("Please enter the three coefficients. \na: "))
b = float(input("b: "))
c = float(input("c: "))

###############################################################################
# Function implementing the standard formula for finding the roots of the
# quadratic equation

def quad_func(a, b, c):
	discriminant = sqrt(b**2 - (4 * a * c))

	x_pos_d = discriminant
	x_neg_d = -discriminant

	results = array([x_pos_d, x_neg_d])

	results += (-b)
	results /= (2 * a)

	return sort(results)

###############################################################################
# Function implementing the a different, but equivalent formula for finding the
# roots of the quadratic equation

def sec_q_func(a, b, c):
	numerator = 2 * c
	discriminant = sqrt(b**2 - (4 * a * c))

	plus_denom = -b + discriminant
	neg_denom = -b - discriminant

	results = array([(numerator / plus_denom), (numerator / neg_denom)])

	return sort(results)

###############################################################################
# Calculating the roots and printing them
# Printing all of the digits in order to best see where the roots differ

func_1_res = quad_func(a, b, c)

func_2_res = sec_q_func(a, b, c)

print("\nSolutions, Method 1: ({0}, {1})".format(func_1_res[0],
      func_1_res[1]))

print("\nSolutions, Method 2: ({0}, {1})".format(func_2_res[0], func_2_res[1]))

# END PROGRAM


# Exercise 5.15
# Author: Noah Waterfield Price

import numpy as np
import matplotlib.pyplot as plt

def L_k(x, k, xp, yp):
    ans = 1
    for i in range(len(xp)):
        if i != k:
            ans *= (x - xp[i]) / (xp[k] - xp[i])
    return ans

def p_L(x, xp, yp):
    ans = 0
    for k in range(len(yp)):
        ans += yp[k] * L_k(x, k, xp, yp)
    return ans

def verify(xp, yp):
    status = 'Verified!'
    for k in range(len(xp)):
        if abs(p_L(xp[k], xp, yp) - yp[k]) > 1e-15:
            status = 'Error!'
            break
    print status

def verbose_verify(xp, yp):
    print '%10s %10s %10s %12s' % ('x', 'Exact', 'Approx', 'Difference')
    for k in range(len(xp)):
        x = xp[k]
        exact = yp[k]
        approx = p_L(xp[k], xp, yp)
        diff = abs(p_L(xp[k], xp, yp) - yp[k])
        print '%10f %10f %10f %12f' % (x, exact, approx, diff)

def graph(f, n, xmin, xmax, resolution=1001):
    xlist = np.linspace(xmin, xmax, n)
    ylist = f(xlist)
    xlist_fine = np.linspace(xmin, xmax, resolution)
    ylist_fine = p_L(xlist_fine, xlist, ylist)
    plt.plot(xlist, ylist, 'ro')
    plt.plot(xlist_fine, ylist_fine)

def annotate_graph():
    ax = plt.gca()
    ax.set_xlabel('x')
    ax.set_ylabel('f(x)')
    ax.legend(['f(x)', 'Interpolated'])

if __name__ == '__main__':
    xlist = np.linspace(0, np.pi, 5)
    ylist = np.sin(xlist)
    verbose_verify(xlist, ylist)
    graph(np.sin, 5, 0, np.pi)
    annotate_graph()
    plt.show()

"""
Sample run:
python Lagrange_poly2.py
         x      Exact     Approx   Difference
  0.000000   0.000000   0.000000     0.000000
  0.785398   0.707107   0.707107     0.000000
  1.570796   1.000000   1.000000     0.000000
  2.356194   0.707107   0.707107     0.000000
  3.141593   0.000000   0.000000     0.000000
"""


import numpy as np
from scipy.stats import kstest, norm

class LinearRegression(object):
    '''Ordinary Least Squares...'''
    def __init__(self, X, y, fit_intercept=True):
        self.X = X
        self.y = y
        self.fit_intercept = fit_intercept
        self._coeff = None

    def fit(self, check_residuals=True, threshold=0.05):
        if self.fit_intercept:
            self.X = self._add_intercept(self.X)
        self._solve_ols()

        if check_residuals:
            print 'checking residuals...'
            if self._check_residuals(threshold):
                print '...residuals are gaussian distributed at %3.2f...' % threshold
            else:
                print '...residuals are Not gaussian distributed...'

    def _add_intercept(self, X):
        '''add a column of 1s in the X matrix...'''
        return np.insert(X, 0, np.ones_like(X[:, 0]), axis=1)

    def _solve_ols(self):
        '''matrix solution for OLS...'''
        XT = self.X.transpose()
        XTX = np.dot(XT, self.X)
        XTX_i = np.linalg.inv(XTX)
        self._coeff = np.dot(np.dot(XTX_i, XT), self.y)

    def _calculate_residuals(self):
        return self.y - np.dot(self.X, self._coeff)

    def _check_residuals(self, threshold=0.05):
        '''check residuals using ks_test for normality...'''
        residuals = self._calculate_residuals()
        mu, std = np.mean(residuals), np.std(residuals)

        def g_cdf(x):
            return norm.cdf(x, mu, std)

        # standard 2-sided ks test...
        t_stat, p_value = kstest(residuals, g_cdf)
        # returns True for gaussian noise
        return p_value > threshold

    def calc_r_squared(self, adjusted=True):
        '''returns the standard R2 value...'''
        n_obs, n_var = self.X.shape
        y_ = np.mean(self.y)
        p = np.dot(self.X, self._coeff)

        ss_t = np.sum(np.square(self.y - y_))
        ss_e = np.sum(np.square(self.y - p))

        r2 = 1.0 - ss_e/ss_t
        if adjusted:
            return 1.0 - (1.0 - r2) * ((n_obs - 1) / (n_obs - n_var))
        return r2


import numpy as np

def l2(W, alpha=1.0):
    '''
    calculate the l2 penalty (used in ridge regression) for the input weights W

    this is for educational purposes to compare l1 and l2 penalty on different weights

    we expect to see l2 to produce a lower penalty when all values are minimized

    the l2 penalty formula is:

    alpha * sum(W**2)
    '''
    return alpha * (W**2).sum()

def l1(W, alpha=1.0):
    '''
    calculate the l1 penalty (used in lasso regression) for the input weights W

    this is for educational purposes to compare l1 and l2 penalty on different weights

    we expect to see l1 to produce a lower penalty when some values are zero

    the l1 penalty formula is:

    alpha * sum(abs(W))
    '''
    return alpha * abs(W).sum()


# # Chapter 3: Sampling, Convolution and Discrete Fourier Transform

# Author: Sandipan Dey

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

# ## Problems

import numpy as np
import numpy.fft as fp
from scipy import signal
import scipy.fftpack
from skimage.io import imread
from skimage.color import rgb2gray
from skimage.metrics import peak_signal_noise_ratio
from scipy.ndimage import convolve
import cv2
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D  # noqa: F401 unused import
from mpl_toolkits.axes_grid1 import make_axes_locatable
from matplotlib.ticker import LinearLocator, FormatStrFormatter

def plot_image(im, title):
    plt.imshow(im, cmap='gray')
    plt.axis('off')
    plt.title(title, size=20)

def plot_freq_spectrum(F, title, cmap=plt.cm.gray, show_axis=True, colorbar=False):
    plt.imshow((20*np.log10(0.1 + fp.fftshift(F))).real.astype(int), cmap=cmap)
    if not show_axis:
        plt.axis('off')
    if colorbar:
        plt.colorbar()
    plt.title(title, size=20)

h, w = 100, 100
images = list()
im = np.zeros((h,w))
for x in range(h):
    im[x,:] = np.sin(x)
images.append(im)
im = np.zeros((h,w))
for y in range(w):
    im[:,y] = np.sin(y)
images.append(im)

im = np.zeros((h,w))
for x in range(h):
    for y in range(w):
        im[x,y] = np.sin(x + y)
images.append(im)
im = np.zeros((h,w))
for x in range(h):
    for y in range(w):
        if (x-h/2)**2 + (y-w/2)**2 < 100:
            im[x,y] = np.sin(x + y)
images.append(im)

im = np.zeros((h,w))
for x in range(h):
    for y in range(w):
        if (x-h/2)**2 + (y-w/2)**2 < 25:
            im[x,y] = 1
images.append(im)
im = np.zeros((h,w))
im[h//2 -5:h//2 + 5, w//2 -5:w//2 + 5] = 1
images.append(im)

plt.figure(figsize=(25,10))
i = 1
for im in images:
    plt.subplot(2,6,i), plot_image(im, 'image {}'.format(i))
    plt.subplot(2,6,i+6), plot_freq_spectrum(fp.fft2(im), 'DFT {}'.format(i), show_axis=False)
    i += 1
plt.tight_layout()
plt.show()

im = rgb2gray(imread("images/Img_03_01.jpg"))
h, w = im.shape
F = fp.fft2(im)
F_shifted = fp.fftshift(F)

xs = list(map(int, np.linspace(1, h//5, 10)))
ys = list(map(int, np.linspace(1, w//5, 10)))
plt.figure(figsize=(20,8))
plt.gray()
for i in range(10):
    F_mask = np.zeros((h, w))
    F_mask[h//2-xs[i]:h//2+xs[i]+1, w//2-ys[i]:w//2+ys[i]+1] = 1
    F1 = F_shifted*F_mask
    im_out =  fp.ifft2(fp.ifftshift(F1)).real #np.abs()
    plt.subplot(2,5,i+1), plt.imshow(im_out), plt.axis('off')
    plt.title('{}x{},PSNR={}'.format(2*xs[i]+1, 2*ys[i]+1, round(peak_signal_noise_ratio(im, im_out),2)), size=15)
plt.suptitle('Fourier reconstruction by keeping first few frequency basis vectors', size=25)
plt.show()

# ## 1. Sampling to increase/decrease the resolution of an image

# ### 1.1. Up-sampling an image by using DFT and a Low-pass-filter (LPF)

im = 255*rgb2gray(imread('images/Img_03_01.jpg'))
im1 = np.zeros((2*im.shape[0], 2*im.shape[1]))
print(im.shape, im1.shape)
for i in range(im.shape[0]):
    for j in range(im.shape[1]):
        im1[2*i,2*j] = im[i,j]

kernel = [[0.25, 0.5, 0.25], [0.5, 1, 0.5], [0.25, 0.5, 0.25]]

def pad_with_zeros(vector, pad_width, iaxis, kwargs):
    vector[:pad_width[0]] = 0
    vector[-pad_width[1]:] = 0
    return vector

kernel = np.pad(kernel, (((im1.shape[0]-3)//2,(im1.shape[0]-3)//2+1), ((im1.shape[1]-3)//2,(im1.shape[1]-3)//2+1)), pad_with_zeros)

freq = fp.fft2(im1)
freq_kernel = fp.fft2(fp.ifftshift(kernel))

freq_LPF = freq*freq_kernel # by the Convolution theorem

im2 = fp.ifft2(freq_LPF).real

plt.figure(figsize=(15,10))
plt.gray() # show the filtered result in grayscale
cmap = 'nipy_spectral' #'viridis'
plt.subplot(231), plot_image(im, 'Original Input Image')
plt.subplot(232), plot_image(im1, 'Padded Input Image')
plt.subplot(233), plot_freq_spectrum(freq, 'Original Image Spectrum', cmap=cmap)
plt.subplot(234), plot_freq_spectrum(freq_kernel, 'Image Spectrum of the LPF', cmap=cmap)
plt.subplot(235), plot_freq_spectrum(fp.fft2(im2), 'Image Spectrum after LPF', cmap=cmap)
plt.subplot(236), plot_image(im2.astype(np.uint8), 'Output Image')
plt.show()

# ### 1.2. Down-sampling with Anti-Aliasing using Gaussian Filter

from skimage.filters import gaussian
from skimage import img_as_float
#im = rgb2gray(imread('images/Img_03_03.jpg'))
im = img_as_float(imread('images/Img_03_08.jpg'))
print(im.shape)

im_blurred = gaussian(im, sigma=1.25, multichannel=True)

n = 4 # create and image 16 times smaller in size
h, w = im.shape[0] // n, im.shape[1] // n
im_small = np.zeros((h, w, 3))
for i in range(h):
   for j in range(w):
      im_small[i,j] = im[n*i, n*j]
im_small_aa = np.zeros((h, w, 3))
for i in range(h):
   for j in range(w):
      im_small_aa[i,j] = im_blurred[n*i, n*j]

plt.figure(figsize=(15,15))
plt.imshow(im), plt.title('Original Image', size=15)
plt.show()

plt.figure(figsize=(15,15))
plt.imshow(im_small), plt.title('Resized Image (without Anti-aliasing)', size=15)
plt.show()

plt.figure(figsize=(15,15))
plt.imshow(im_small_aa), plt.title('Resized Image (with Anti-aliasing)', size=15)
plt.show()

# ## 2. Denoising an Image with LPF/Notch filter in the Frequency domain

# ### 2.1 Removing Periodic Noise with Notch Filter

im_noisy = rgb2gray(imread("images/Img_03_23.jpg"))
F_noisy = fp.fft2((im_noisy))
print(F_noisy.shape)

def plot_freq_spectrum(F, title, cmap=plt.cm.gray):
    plt.imshow((20*np.log10(0.1 + fp.fftshift(F))).real.astype(int), cmap=cmap)
    plt.xticks(np.arange(0, im.shape[1], 25))
    plt.yticks(np.arange(0, im.shape[0], 25))
    plt.title(title, size=20)

plt.figure(figsize=(20,10))

plt.subplot(121), plot_image(im_noisy, 'Noisy Input Image')
plt.subplot(122), plot_freq_spectrum(F_noisy, 'Noisy Image Spectrum')

plt.tight_layout()
plt.show()

F_noisy_shifted = fp.fftshift(F_noisy)
F_noisy_shifted[180,210] = F_noisy_shifted[200,190] = 0
im_out =  fp.ifft2(fp.ifftshift(F_noisy_shifted)).real #np.abs()

#print(signaltonoise(im1, axis=None))
plt.figure(figsize=(10,8))
plot_image(im_out, 'Output Image')
plt.show()

# ### 2.2 Removing salt-and-pepper noise using Gaussian LPF with scipy fftpack

from scipy import ndimage
from scipy import fftpack
from skimage.util import random_noise

im = rgb2gray(imread('images/Img_03_02.jpg'))
noisy = random_noise(im, mode='s&p')

im_freq = fftpack.fft2(im)
noisy_freq = fftpack.fft2(noisy)
sigma = 1 #0.1
noisy_smoothed_freq = ndimage.fourier_gaussian(noisy_freq, sigma=sigma)
noisy_smoothed = fftpack.ifft2(noisy_smoothed_freq)

fig, ((ax1, ax2), (ax3, ax4), (ax5, ax6)) = plt.subplots(3, 2, figsize=(20,20))
plt.gray()  # show the filtered result in grayscale
ax1.imshow(im), ax1.axis('off'), ax1.set_title('Original Image', size=20)
ax2.imshow((20*np.log10(0.1 + fftpack.fftshift(im_freq))).real.astype(int))
ax2.set_title('Original Image (Freq Spec)', size=20)
ax3.imshow(noisy), ax3.axis('off'), ax3.set_title('Noisy Image', size=20)
ax4.imshow((20*np.log10( 0.1 + fftpack.fftshift(noisy_freq))).real.astype(int))
ax4.set_title('Noisy Image (Freq Spec)', size=20)

ax5.imshow(noisy_smoothed.real), ax5.axis('off'), ax5.set_title('Output Image (with LPF)', size=20)
ax6.imshow( (20*np.log10( 0.1 + fftpack.fftshift(noisy_smoothed_freq))).real.astype(int))
ax6.set_title('Output Image (Freq Spec)', size=20)
plt.tight_layout()
plt.show()

# ## 3. Blurring an Image with an LPF in the Frequency domain

# ### 3.0 Different Blur Kernels and Convolution in the Frequency domain

def get_gaussian_edge_blur_kernel(sigma, sz=15):
    # First create a 1-D Gaussian kernel
    x = np.linspace(-10, 10, sz)
    kernel_1d = np.exp(-x**2/sigma**2)
    kernel_1d /= np.trapz(kernel_1d) # normalize the sum to 1.0
    # create a 2-D Gaussian kernel from the 1-D kernel
    kernel_2d = kernel_1d[:, np.newaxis] * kernel_1d[np.newaxis, :]
    return kernel_2d

def get_motion_blur_kernel(ln, angle, sz=15):
    kern = np.ones((1, ln), np.float32)
    angle = -np.pi*angle/180
    c, s = np.cos(angle), np.sin(angle)
    A = np.float32([[c, -s, 0], [s, c, 0]])
    sz2 = sz // 2
    A[:,2] = (sz2, sz2) - np.dot(A[:,:2], ((ln-1)*0.5, 0))
    kern = cv2.warpAffine(kern, A, (sz, sz), flags=cv2.INTER_CUBIC)
    return kern

def get_out_of_focus_kernel(r, sz=15):
    kern = np.zeros((sz, sz), np.uint8)
    cv2.circle(kern, (sz, sz), r, 255, -1, cv2.LINE_AA, shift=1)
    kern = np.float32(kern) / 255
    return kern

def dft_convolve(im, kernel):
    F_im = fp.fft2(im)
    #F_kernel = fp.fft2(kernel, s=im.shape)
    F_kernel = fp.fft2(fp.ifftshift(kernel), s=im.shape)
    F_filtered = F_im * F_kernel
    im_filtered = fp.ifft2(F_filtered)
    cmap = 'RdBu'
    plt.figure(figsize=(20,10))
    plt.gray()
    plt.subplot(131), plt.imshow(im), plt.axis('off'), plt.title('input image', size=20)
    plt.subplot(132), plt.imshow(kernel, cmap=cmap), plt.title('kernel', size=20)
    plt.subplot(133), plt.imshow(im_filtered.real), plt.axis('off'), plt.title('output image', size=20)
    plt.tight_layout()
    plt.show()

im = rgb2gray(imread('images/Img_03_03.jpg'))

kernel = get_gaussian_edge_blur_kernel(25, 25)
dft_convolve(im, kernel)

kernel = get_motion_blur_kernel(30, 60, 25)
dft_convolve(im, kernel)

kernel = get_out_of_focus_kernel(15, 20)
dft_convolve(im, kernel)

# ### 3.1 Blurring with scipy.ndimage frequency-domain filters

# #### With fourier_gaussian

im = imread('images/Img_03_31.png')
freq = fp.fft2(im)

fig, axes = plt.subplots(2, 3, figsize=(20,15))
plt.subplots_adjust(0,0,1,0.95,0.05,0.05)
plt.gray() # show the filtered result in grayscale
axes[0, 0].imshow(im), axes[0, 0].set_title('Original Image', size=20)
axes[1, 0].imshow((20*np.log10( 0.1 + fp.fftshift(freq))).real.astype(int)), axes[1, 0].set_title('Original Image Spectrum', size=20)
i = 1
for sigma in [3,5]:
    convolved_freq = ndimage.fourier_gaussian(freq, sigma=sigma)
    convolved = fp.ifft2(convolved_freq).real # the imaginary part is an artifact
    axes[0, i].imshow(convolved)
    axes[0, i].set_title(r'Output with FFT Gaussian Blur, $\sigma$={}'.format(sigma), size=20)
    axes[1, i].imshow((20*np.log10( 0.1 + fp.fftshift(convolved_freq))).real.astype(int))
    axes[1, i].set_title(r'Spectrum with FFT Gaussian Blur, $\sigma$={}'.format(sigma), size=20)
    i += 1
for a in axes.ravel():
    a.axis('off')
plt.show()

# #### With fourier_uniform

im = imread('images/Img_03_31.png')
freq = fp.fft2(im)
freq_uniform = ndimage.fourier_uniform(freq, size=10)

fig, (axes1, axes2) = plt.subplots(1, 2, figsize=(20,10))
plt.gray() # show the result in grayscale
im1 = fp.ifft2(freq_uniform)
axes1.imshow(im), axes1.axis('off')
axes1.set_title('Original Image', size=20)
axes2.imshow(im1.real) # the imaginary part is an artifact
axes2.axis('off')
axes2.set_title('Blurred Image with Fourier Uniform', size=20)
plt.tight_layout()
plt.show()

plt.figure(figsize=(10,10))
plt.imshow( (20*np.log10( 0.1 + fp.fftshift(freq_uniform))).real.astype(int))
plt.title('Frequency Spectrum with fourier uniform', size=20)
plt.show()

# #### With fourier_ellipsoid

freq_ellipsoid = ndimage.fourier_ellipsoid(freq, size=10)
im1 = fp.ifft2(freq_ellipsoid)

fig, (axes1, axes2) = plt.subplots(1, 2, figsize=(20,10))
axes1.imshow(im), axes1.axis('off')
axes1.set_title('Original Image', size=20)
axes2.imshow(im1.real) # the imaginary part is an artifact
axes2.axis('off')
axes2.set_title('Blurred Image with Fourier Ellipsoid', size=20)
plt.tight_layout()
plt.show()

plt.figure(figsize=(10,10))
plt.imshow( (20*np.log10( 0.1 + fp.fftshift(freq_ellipsoid))).real.astype(int))
plt.title('Frequency Spectrum with Fourier ellipsoid', size=20)
plt.show()

# ### 3.2 Gaussian Blur LowPass Filter with scipy.fftpack

#im = rgb2gray(imread('images/Img_03_04.jpg'))
im = rgb2gray(imread('images/Img_03_11.jpg'))
freq = fp.fft2(im)

kernel = np.outer(signal.gaussian(im.shape[0], 1), signal.gaussian(im.shape[1], 1))
#assert(freq.shape == kernel.shape)

freq_kernel = fp.fft2(fp.ifftshift(kernel))

convolved = freq*freq_kernel # by the Convolution theorem

im_blur = fp.ifft2(convolved).real
im_blur = 255 * im_blur / np.max(im_blur)

plt.figure(figsize=(20,20))
plt.subplot(221), plt.imshow(kernel, cmap='coolwarm'), plt.colorbar()
plt.title('Gaussian Blur Kernel', size=20)

plt.subplot(222)
plt.imshow( (20*np.log10( 0.01 + fp.fftshift(freq_kernel))).real.astype(int), cmap='inferno')
plt.colorbar()
plt.title('Gaussian Blur Kernel (Freq. Spec.)', size=20)
plt.subplot(223), plt.imshow(im, cmap='gray'), plt.axis('off'), plt.title('Input Image', size=20)
plt.subplot(224), plt.imshow(im_blur, cmap='gray'), plt.axis('off'), plt.title('Output Blurred Image', size=20)
plt.tight_layout()
plt.show()

def plot_3d(X, Y, Z, cmap=plt.cm.seismic):
    fig = plt.figure(figsize=(20,20))
    ax = fig.gca(projection='3d')
    # Plot the surface.
    surf = ax.plot_surface(X, Y, Z, cmap=cmap, linewidth=5, antialiased=False)
    #ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10)
    #ax.set_zscale("log", nonposx='clip')
    #ax.zaxis.set_scale('log')
    ax.zaxis.set_major_locator(LinearLocator(10))
    ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))
    ax.set_xlabel('F1', size=30)
    ax.set_ylabel('F2', size=30)
    ax.set_zlabel('Freq Response', size=30)
    #ax.set_zlim((-40,10))
    # Add a color bar which maps values to colors.
    fig.colorbar(surf) #, shrink=0.15, aspect=10)
    #plt.title('Frequency Response of the Gaussian Kernel')
    plt.show()

Y = np.arange(freq.shape[0]) #-freq.shape[0]//2,freq.shape[0]-freq.shape[0]//2)
X = np.arange(freq.shape[1]) #-freq.shape[1]//2,freq.shape[1]-freq.shape[1]//2)
X, Y = np.meshgrid(X, Y)
Z = (20*np.log10( 0.01 + fp.fftshift(freq_kernel))).real
plot_3d(X,Y,Z)

Z = (20*np.log10( 0.01 + fp.fftshift(freq))).real
plot_3d(X,Y,Z)

Z = (20*np.log10( 0.01 + fp.fftshift(convolved))).real
plot_3d(X,Y,Z)

# ### 3.3 Convolution in frequency domain with a colored image using fftconvolve from scipy signal

from skimage import img_as_float
from scipy import signal
im = img_as_float(plt.imread('images/Img_03_07.jpg'))

kernel = get_gaussian_edge_blur_kernel(sigma=10, sz=15)
im1 = signal.fftconvolve(im, kernel[:, :, np.newaxis], mode='same')
im1 = im1 / np.max(im1)

kernel = np.array([[0,-1,0],[-1,4,-1],[0,-1,0]])
im2 = signal.fftconvolve(im, kernel[:, :, np.newaxis], mode='same')
im2 = im2 / np.max(im2)
im2 = np.clip(im2, 0, 1)

plt.figure(figsize=(20,10))
plt.subplot(131), plt.imshow(im), plt.axis('off'), plt.title('original image', size=20)
plt.subplot(132), plt.imshow(im1), plt.axis('off'), plt.title('output with Gaussian LPF', size=20)
plt.subplot(133), plt.imshow(im2), plt.axis('off'), plt.title('output with Laplacian HPF', size=20)
plt.tight_layout()
plt.show()

# ## 4. Edge Detection with High-Pass Filters (HPF) in the Frequency domain

def dft2(im):

    freq = cv2.dft(np.float32(im), flags = cv2.DFT_COMPLEX_OUTPUT)
    freq_shift = np.fft.fftshift(freq)
    mag, phase = freq_shift[:,:,0], freq_shift[:,:,1]

    return mag + 1j*phase

def idft2(freq):

    real, imag = freq.real, freq.imag
    back = cv2.merge([real, imag])
    back_ishift = np.fft.ifftshift(back)
    im = cv2.idft(back_ishift, flags=cv2.DFT_SCALE)
    im = cv2.magnitude(im[:,:,0], im[:,:,1])

    return im

def ideal(sz, D0):
    h, w = sz
    u, v = np.meshgrid(range(-w//2,w//2), range(-h//2,h//2)) #, sparse=True)
    return np.sqrt(u**2 + v**2) > D0

def gaussian(sz, D0):
    h, w = sz
    u, v = np.meshgrid(range(-w//2,w//2), range(-h//2,h//2)) #, sparse=True)
    return 1-np.exp(-(u**2 + v**2)/(2*D0**2))

def butterworth(sz, D0, n=1):
    h, w = sz
    u, v = np.meshgrid(range(-w//2,w//2), range(-h//2,h//2)) #, sparse=True)
    return 1 / (1 + (D0/(0.01+np.sqrt(u**2 + v**2)))**(2*n))

def plot_HPF(im, f, D0s):
    freq = dft2(im)
    fig = plt.figure(figsize=(20,20))
    plt.subplots_adjust(0,0,1,0.95,0.05,0.05)
    i = 1
    for D0 in D0s:
        freq_kernel = f(im.shape, D0)
        convolved = freq*freq_kernel # by the Convolution theorem
        im_convolved = idft2(convolved).real
        im_convolved = (255 * im_convolved / np.max(im_convolved)).astype(np.uint8)
        plt.subplot(2,2,i)
        last_axes = plt.gca()
        img = plt.imshow((20*np.log10(0.01 + freq_kernel)).astype(int), cmap='coolwarm')
        divider = make_axes_locatable(img.axes)
        cax = divider.append_axes("right", size="5%", pad=0.05)
        fig.colorbar(img, cax=cax)
        plt.sca(last_axes), plt.title('{} HPF Kernel (freq)'.format(f.__name__), size=20)
        plt.subplot(2,2,i+2), plt.imshow(im_convolved), plt.axis('off')
        plt.title(r'output with {} HPF ($D_0$={})'.format(f.__name__, D0), size=20)
        i += 1
    plt.show()

def plot_HPF_3d(im, f, D0s):
    freq = dft2(im)
    fig = plt.figure(figsize=(20,10))
    plt.subplots_adjust(0,0,1,0.95,0.05,0.05)
    i = 1
    for D0 in D0s:
        freq_kernel = f(im.shape, D0)
        convolved = freq*freq_kernel # by the Convolution theorem
        Y = np.arange(freq_kernel.shape[0])
        X = np.arange(freq_kernel.shape[1])
        X, Y = np.meshgrid(X, Y)
        Z = (20*np.log10( 0.01 + convolved)).real
        ax = fig.add_subplot(1, 2, i, projection='3d')
        surf = ax.plot_surface(X, Y, Z, cmap=plt.cm.coolwarm, linewidth=0, antialiased=False)
        ax.zaxis.set_major_locator(LinearLocator(10)), ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))
        ax.set_xlabel('F1', size=30), ax.set_ylabel('F2', size=30)
        plt.title(r'output with {} HPF (freq)'.format(f.__name__, D0), size=20)
        fig.colorbar(surf, shrink=0.5, aspect=10)
        i += 1
    plt.show()

def plot_filter_3d(sz, f, D0s, cmap=plt.cm.coolwarm):
    fig = plt.figure(figsize=(20,10))
    plt.subplots_adjust(0,0,1,0.95,0.05,0.05)
    i = 1
    for D0 in D0s:
        freq_kernel = f(sz, D0)
        Y = np.arange(freq_kernel.shape[0])
        X = np.arange(freq_kernel.shape[1])
        X, Y = np.meshgrid(X, Y)
        Z = (20*np.log10( 0.01 + freq_kernel)).real
        ax = fig.add_subplot(1, 3, i, projection='3d')
        surf = ax.plot_surface(X, Y, Z, cmap=cmap, linewidth=0, antialiased=False)
        ax.zaxis.set_major_locator(LinearLocator(10))
        ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))
        ax.set_xlabel('F1', size=30)
        ax.set_ylabel('F2', size=30)
        ax.set_title('{} HPF Kernel (freq)'.format(f.__name__), size=20)
        fig.colorbar(surf, shrink=0.5, aspect=10)
        i += 1

im = plt.imread('images/Img_03_12.png')
im = rgb2gray(im)
plt.figure(figsize=(7,12))
plt.imshow(im), plt.axis('off'), plt.title('original image')
plt.show()

D0 = [10, 30]

plot_HPF(im, ideal, D0)

plot_HPF_3d(im, ideal, D0)

plot_filter_3d(im.shape, ideal, D0)

plot_HPF(im, gaussian, D0)

plot_HPF_3d(im, gaussian, D0)

plot_filter_3d(im.shape, gaussian, D0)

plot_HPF(im, butterworth, D0)

plot_HPF_3d(im, butterworth, D0)

plot_filter_3d(im.shape, butterworth, D0)

# ## 5. Implement Homomorphic Filters

from skimage.filters import sobel, threshold_otsu

def homomorphic_filter(im, D0, g_l=0, g_h=1, n=1):
    im_log = np.log(im.astype(np.float)+1)
    im_fft = dft2(im_log)
    H = (g_h - g_l) * butterworth(im.shape, D0, n) + g_l
    #H = np.fft.ifftshift(H)
    im_fft_filt = H*im_fft
    #im_fft_filt = np.fft.ifftshift(im_fft_filt)
    im_filt = idft2(im_fft_filt)
    im = np.exp(im_filt.real)-1
    im = np.uint8(255*im/im.max())
    return im

image = rgb2gray(imread('images/Img_03_13.jpg'))
image_filtered = homomorphic_filter(image, D0=30, n=2, g_l=0.3, g_h=1)

image_edges = sobel(image)
image_edges = image_edges <= threshold_otsu(image_edges)

image_filtered_edges = sobel(image_filtered)
image_filtered_edges = image_filtered_edges <= threshold_otsu(image_filtered_edges)

plt.figure(figsize=(21,17))
plt.gray()
plt.subplots_adjust(0,0,1,0.95,0.01,0.05)
plt.subplot(221), plt.imshow(image), plt.axis('off'), plt.title('original image', size=20)
plt.subplot(222), plt.imshow(image_filtered), plt.axis('off'), plt.title('filtered image', size=20)
plt.subplot(223), plt.imshow(image_edges), plt.axis('off'), plt.title('original image edges', size=20)
plt.subplot(224), plt.imshow(image_filtered_edges), plt.axis('off'), plt.title('filtered image edges', size=20)
plt.show()


import numpy as np
import matplotlib.pyplot as plt
import traitlets
from scipy.integrate import ode, odeint
import ipywidgets as widgets
from IPython.display import display, HTML
import matplotlib.animation as animation

##########

# coursebook examples

##########

def ode0_ex_1_1_func(Tobj, t, Troom, alpha):
	dTdt = -1.*alpha*(Tobj-Troom)
	return dTdt

def ode0_ex_1_1(Tini,Troom,alpha,t):
	T = odeint(ode0_ex_1_1_func, Tini, t, args=(Troom, alpha))
	plt.xlabel("t")
	plt.ylabel("T(C)")
	plt.title("Newton's Law of Cooling")
	plt.plot(t,T)

def ode0_ex_1_2_func(theta, t, g, l):
	return [theta[1], -1.*(g/l)*np.sin(theta[0])]

def ode0_ex_1_2(theta1_ini,theta2_ini, t, g, l):
	theta0 = [theta1_ini, theta2_ini]
	theta = odeint(ode0_ex_1_2_func, theta0, t, args=(g,l))
	plt.xlabel("t")
	plt.ylabel("Angular displacement, (radians)")
	plt.title("Pendulum Anglular Displacement")
	plt.plot(t,theta[:,0])

##########

# convolution

##########

#  convolution functions

def shortSW(t):
    return [1*(val > 0 and val < 2) for val in t]

def longSW(t):
    return [1*(val > 0 and val < 4) for val in t]

def poly(t):
    return -0.25*(t)*(t-4)

def convolveStep(func1, func2, tao):
	"""
		Compute one step of convolution

		----------

		Parameters

		----------

		func1: callable
			function to convolve

		func2: callable
			function to convolve

		tao: float
			dummy variable. Shift in t
	"""
	step = np.multiply(func1, func2)

	return np.trapz(step, x=tao)

def convoluteTo(f1, f2, t, tao):
	"""
		Convolve based on t values to tmax

		----------

		Parameters

		----------

		func1: callable
			function to convolve

		func2: callable
			function to convolve

		t: array_like
			time samples

		tao: float
			dummy variable. Shift in t

		----------

		Returns

		----------

		out: array_like
			convolved function
	"""
	out = []
	for i in t:
		out.append(np.trapz(np.multiply(f1(i-tao), f2(tao)), x=tao))

	return out

def convMain(tmax, func):
	"""
		Main function for producing convolution and plotting result

		----------

		Parameters

		----------

		tmax: float
			max t value

		func: string
			function key for selecting function
	"""
	_, (ax1, ax2) = plt.subplots(2, figsize=(16,8))

	f1 = shortSW
	tao = np.linspace(-10, 10, 1000)
	t = np.arange(-5, tmax, 0.5)

	if func=='f1':
		func2 = shortSW(tmax-tao-0.5)
		f2 = shortSW

	elif func=='f2':
		func2 = longSW(tmax-tao-0.5)
		f2 = longSW

	elif func=='f3':
		func2 = poly(tmax-tao-1.5)
		f2 = poly

	func1 = f1(tao)
	conv = convoluteTo(f1, f2, t, tao)

	ax1.plot(tao, func1)
	ax1.plot(tao, func2)
	ax1.set_xlim(-10, 10)
	ax1.set_ylim(0, 1.2)

	ax2.plot(t, conv)
	ax2.set_xlim(-10, 10)
	ax2.set_ylim(0, 4)

	ax1.set_xlabel('Tau')
	ax1.yaxis.set_ticks([])
	ax1.xaxis.set_ticks([])
	ax1.set_title('Functions showing t shift')
	ax2.set_xlabel('t')
	ax2.set_title('Convolution')
	ax2.yaxis.set_ticklabels([])
	ax2.xaxis.set_ticklabels([])

	plt.show()

def Convolution():
	"""
		main convolution funtion called from notebook
	"""
	t_sldr = widgets.FloatSlider(value=-5, min=-5, max=7, step=0.5, description='$t$', continuous_update=False)
	#  f3 doesn't really work properly
	# f_drp = widgets.Dropdown(options=['f1', 'f2', 'f3'])
	f_drp = widgets.Dropdown(options=['f1', 'f2'])

	return widgets.VBox([widgets.HBox([t_sldr, f_drp]), widgets.interactive_output(convMain, {'tmax':t_sldr, 'func':f_drp})])

##########

# numerical methods

##########

#  derivatives and solutions for solver
def dydx1(x, y, *args):
	return -y+1

def y1(x):
	return 1 - np.exp(-x)

def dydx2(x, y, *args):
	return x*np.exp(np.sin(x))

def dydx3(x, y, *args):
	return np.cos(x)

def y3(x):
	return np.sin(x)

def euler_step(f, xk, yk, h, pars=[]):
	''' Compute a single Euler step. From David Dempsey/ENGSCI 233

		Parameters
		----------
		f : callable
			Derivative function.
		xk : float
			Independent variable at beginning of step.
		yk : float
			Solution at beginning of step.
		h : float
			Step size.
		pars : iterable
			Optional parameters to pass to derivative function.

		Returns
		-------
		yk1 : float
			Solution at end of the Euler step.
	'''
	# evaluate derivative at point
	# step by h in direction of derivative
	# return value of new point
	return yk + h * f(xk, yk, *pars)

def euler_solve(f, x0, y0, x1, h, pars = []):
	''' Compute solution of initial value ODE problem using Euler method.
		From David Dempsey/ENGSCI 233

		----------

		Parameters

		----------

		f : callable
			Derivative function.

		x0 : float
			Initial value of independent variable.

		y0 : float
			Initial value of solution.

		x1 : float
			Final value of independent variable.

		h : float
			Step size.

		pars : iterable
			Optional parameters to pass into derivative function.

		----------

		Returns

		----------

		xs : array-like
			Independent variable at solution.
		ys : array-like
			Solution.

	'''

	# initialise
	nx = int(np.ceil((x1-x0)/h))		# compute number of Euler steps to take
	xs = x0+np.arange(nx+1)*h			# x array
	ys = 0.*xs							# array to store solution
	ys[0] = y0							# set initial value

	for i in range(len(xs)-1):
		ys[i+1] = euler_step(f, xs[i], ys[i], h, pars)

	return xs, ys

def improved_euler_step(f, xk, yk, h):
	"""
		Completes one step of the improved euler method

		----------

		Parameters

		----------

		f: callable
			differential equation to solve

		xk: float
			x ordinate for step start

		tk: float
			y ordinate for step start

		h: float
			step size

		----------

		Returns

		----------
		float: Approximated yk+1 value

	"""

	return yk + h * 0.5*(f(xk, yk) + f(xk + h, yk + h * f(xk, yk)))

def improved_euler_solve(f, x0, y0, x1, h):
	"""
		solves a differential equation using improved euler

		----------

		Parameters

		----------

		f: callable
			differential equation to solve

		x0: float
			inital x value

		y0: float
			inital y value

		x1: float
			upper x value

		h: float
			step size

		----------

		Returns

		----------
		xs: array_like
			x time steps used

		ys: approximated y values

	"""
	# initialise
	nx = int(np.ceil((x1-x0)/h))		# compute number of Euler steps to take
	xs = x0+np.arange(nx+1)*h			# x array
	ys = 0.*xs							# array to store solution
	ys[0] = y0							# set initial value

	for i in range(len(xs)-1):
		# solve for next value in function
		ys[i+1] = improved_euler_step(f, xs[i], ys[i], h)

	return xs, ys

def runge_step(f, xk, yk, h, *args):
	"""
		completes a single step of the runge cutter method (K4)

		----------

		Parameters

		----------

		f: callable
			differential equation to solve

		xk: float
			x ordinate for step start

		tk: float
			y ordinate for step start

		h: float
			step size

		----------

		Returns

		----------

		float: Approximated yk+1 value
	"""
	k1 = h*f(xk, yk, *args)
	k2 = h*f(xk + h/2, yk + k1/2, *args)
	k3 = h*f(xk + h/2, yk + k2/2, *args)
	k4 = h*f(xk + h, yk + k3, *args)

	return yk + (1/6)*(k1 + 2*k2 + 2*k3 + k4)

def runge_solve(f, x0, y0, x1, h, *args):
	'''
		solves a differential equation using runge cutter

		----------

		Parameters

		----------

		f: callable
			differential equation to solve

		x0: float
			x inital condition

		y0: float
			y initial condition

		x1: float
			x stopping point

		h: float
			step size

		----------

		Returns

		----------

		xs: array_like
			x time steps used

		ys: approximated y values
	'''
	nx = int(np.ceil((x1-x0)/h))
	xs = x0+np.arange(nx+1)*h
	ys = 0.*xs
	ys[0] = y0

	for i in range(len(xs)-1):
		ys[i+1] = runge_step(f, xs[i], ys[i], h, args)

	return xs, ys

def plotTangentAtPoint(ax, f, x0, y0, xMin, xMax, h, label=None, color='b'):
	''' plots line on axis given function derivative and x/y points

		----------

		Parameters

		----------

		ax: matplotlib object
			axis to plot on
		f: callable
			derivative function
		x: float
			x ordinate of point
		y: float
			y ordinate of point

	'''
	# plots in range 0 < x < 10

	x = np.arange(xMin,xMax,1)
	m = f(x0,y0)
	y = m*(x-x0) + y0
	if label is not None:
		ax.plot(x,y, ls='--', linewidth=1, label=label, color=color)
	else:
		ax.plot(x,y, ls='--', linewidth=1, color=color)

def RunMethod(h, steps, eqnKey, method, showSln, showGrad):
	''' Runs demo of particular numerical method show

		----------

		Parameters

		----------

        h: float
			step size

        steps: int
			no. steps to take

        eqnKey: str
			key for dict of equtions to use

        method: str
			string key for which method to use

        showSln: bool
			bool for whether to show analytic sln

        showGrad: bool
			bool for whether to show ext grad evals
	'''

	xMin = 0
	xMax = 10
	yMin = 0

	_ = plt.figure(figsize=(16,8))
	ax = plt.axes()
	# get equation
	equation = {"dydx = -y +1":[dydx1, y1, 0, xMax, 0, 1.2], "dy/dx = cos(x)":[dydx3, y3, 0, xMax, -1.5, 1.5]}
	f, soln, xMin, xMax, yMin, yMax = equation[eqnKey]

	xStop = h * steps
	if xStop > xMax: xStop = xMax

	if method=='Improved Euler': # call to improved euler
		x, y = improved_euler_solve(f, 0, 0, xStop, h)
		# for displaying next gradient evaluations
		if showGrad:
			plotTangentAtPoint(ax,f,x[-1]+h,euler_step(f, x[-1], y[-1], h), x[-1]+h, xMax,h, label='Corrector Step', color='r')
			plotTangentAtPoint(ax,f,x[-1],y[-1], x[-1], xMax,h, label='Predictor Step', color='b')
	elif method=='Euler':# call to euler
		x, y = euler_solve(f, 0, 0, xStop, h)
		# for displaying next gradient evaluations
		if showGrad:
			plotTangentAtPoint(ax,f,x[-1],y[-1], xMin, xMax,h, label='Gradient Evaluation')
	elif method=='Runge Cutter': # call to runge cutter
		x, y = runge_solve(f, 0, 0, xStop, h)

	# plot numerical
	ax.plot(x, y, marker='o',color='k', label='Numerical Solution')

	# generate values for error
	xPlot = np.arange(0, xMax, 0.1)
	ySoln = soln(xPlot)
	error = sum((soln(x)[:len(y)]-y)**2)
	# add text showing error
	ax.text(0.01,0.95, 'Total sum of squares error = {:.2e}'.format(error), transform=ax.transAxes, size=20)

	# plot analytic
	if showSln: ax.plot(xPlot,ySoln, label='Analytic Solution')

	plt.xlim(xMin, xMax)
	plt.ylim(yMin, yMax)
	ax.legend(loc=1)
	plt.show()

def NumericalMethods():
	"""
		Main function for called by notebook to produce plots and UI
	"""
	steps_sldr = widgets.IntSlider(value=3, description='Steps', min=0, max=50, continuos_update=False)
	h_sldr = widgets.FloatSlider(value=0.5, description='Step Size', min=0.1, max=1, step=0.1,)
	solver_drp = widgets.Dropdown(options=['Euler', 'Improved Euler'], description='Solver Type')
	eqn_drp = widgets.Dropdown(options=["dydx = -y +1", "dy/dx = cos(x)"], description='Derivative')
	showSln_chk = widgets.Checkbox(value=False, description='Show Solution')
	grad_chk = widgets.Checkbox(value=False, description='Show Next Gradient Evaluations')

	return widgets.VBox([
		widgets.VBox([
			widgets.HBox([
				h_sldr,
				eqn_drp,
				grad_chk]),
			widgets.HBox([
				steps_sldr,
				solver_drp,
				showSln_chk])]),
		widgets.interactive_output(RunMethod, {
			'h':h_sldr,
			'steps':steps_sldr,
			'method':solver_drp,
			'eqnKey':eqn_drp,
			'showSln':showSln_chk,
			'showGrad':grad_chk})
			])

##########

# laplace transforms

# t* = function in t domain
# s* function in s domain

# probably a much better way of doing this. Maybe function class with laplace and t attributes?

##########

def tPoly(t, n, tShift, sShift):
	full = (t-tShift)**n*np.exp(sShift*t)
	return [0 if ((t[i] < 0) or (t[i] < tShift)) else full[i] for i in range(len(t))]

def sPoly(s, n, tShift, sShift):
	full = np.exp(-tShift*s)*np.math.factorial(n)/(s-sShift)**(n+1)
	return [0 if ((s[i] < 0) or (s[i] < sShift)) else full[i] for i in range(len(s))]

def tSin(t, w, tShift, sShift):
	full = np.sin(w*(t-tShift))*np.exp(sShift*t)
	return [0 if ((t[i] < 0) or (t[i] < tShift)) else full[i] for i in range(len(t))]

def sSin(s, w, tShift, sShift):
	full = np.exp(-tShift*s)*(w/((s-sShift)**2 + w**2))
	return [0 if ((s[i] < 0) or (s[i] < sShift)) else full[i] for i in range(len(s))]

def tCos(t, w, tShift, sShift):
	full = np.cos(w*(t-tShift))*np.exp(sShift*t)
	return [0 if ((t[i] < 0) or (t[i] < tShift)) else full[i] for i in range(len(t))]

def sCos(s, w, tShift, sShift):
	full = np.exp(-tShift*s)*(s-sShift)/((s-sShift)**2 + w**2)
	return [0 if ((s[i] < 0) or (s[i] < sShift)) else full[i] for i in range(len(s))]

def tExp(t, a, tShift, sShift):
	full = np.exp(a*(t-tShift))*np.exp(sShift*t)
	return [0 if ((t[i] < 0) or (t[i] < tShift)) else full[i] for i in range(len(t))]

def sExp(s, a, tShift, sShift):
	full = np.exp(-tShift*s)/((s-sShift)-a)
	return [0 if ((s[i] < 0) or (s[i] < sShift)) else full[i] for i in range(len(s))]

def Laplace():
	"""
		Main function called by notebook to produce plots and UI
	"""

	# for ax lim sliders
	tAbsLim = 20
	sAbsLim = 20

	# define widgets
	tlim_sldr = widgets.FloatRangeSlider(value=[-10, 10], step=0.1, min=-tAbsLim, max=tAbsLim, description='t lim', continuous_update=False)
	slim_sldr = widgets.FloatRangeSlider(value=[-10, 10], step=0.1, min=-sAbsLim, max=sAbsLim, description='s lim', continuous_update=False)
	fslim_sldr = widgets.FloatRangeSlider(value=[0, 5], step=0.1, min=-5, max=5, description='F(s) lim', continuous_update=False)
	tShift_sldr = widgets.FloatSlider(value=0, step=0.1, min=-2, max=2, description='Shift t', continuous_updating=False)
	sShift_sldr = widgets.FloatSlider(value=0, step=0.1, min=-2, max=2, description='Shift s', continuous_updating=False)
	func_drop = widgets.Dropdown(options={'t^n':'Poly', 'sin(w*t)':'Sine', 'cos(w*t)':'Cos', 'exp(at)':'exp'}, layout=widgets.Layout(width='220px'), description='$f(t)=$')

	# display
	display(widgets.VBox([
		widgets.HBox([
			widgets.VBox([
				tlim_sldr,
				slim_sldr
			]),
			widgets.VBox([
				tShift_sldr,
				sShift_sldr
			]),
			widgets.VBox([
				fslim_sldr,
				func_drop
			])
		]),
		widgets.interactive_output(showLaplace, {
			'slim':slim_sldr,
			'tlim':tlim_sldr,
			'func':func_drop,
			'tShift':tShift_sldr,
			'fslim':fslim_sldr,
			'sShift':sShift_sldr
			})
		]))

def computeTransform(t, s, slim, tlim, var, tfunc, sfunc, tShift, sShift, fslim):
	"""
		Provides function for interactive output of n or omega to work

		----------

		Parameters

		----------

		t: array_like
			time values

		s: array_like
			s values

		slim: array_like
			pair of s limit values

		tlim: array_like
			pair of t limit values

		var: float
			variable storing either omega or n

		tfunc: callable
			t domain function

		sfunc: callable
			s domain function

		tShift: float
			shift in t

		sShift: float
			 shift in s

		fslim: array_like
			s domain y lim pair
	"""

	ft = tfunc(t, var, tShift, sShift)
	fs = sfunc(s, var, tShift, sShift)

	plotLaplace(t, s, ft, fs, tlim, slim, fslim)

def showLaplace(slim, tlim, func, tShift, sShift, fslim):
	"""
		Chooses function and transform to plot. Produces t samples

		----------

		Parameters

		----------

		slim: array_like
			pair of points for s limits

		tlim: array_like
			pair of points for t limits

		func: str
			string for selected function

		tShift: float
			shift in t

		sShift: float
			 shift in s

		fslim: array_like
			s domain y lim pair
	"""
	t = np.arange(-20, 20, 0.01)
	# s = np.arange(-50, 50 1)
	s = t.copy()
	if func=='Poly':
		var_sldr=widgets.IntSlider(1, min=0, max=10, step=1, description='$n$', continuous_update=False)
		tfunc = tPoly
		sfunc = sPoly

	elif func=='Sine':
		var_sldr=widgets.FloatSlider(1, min=0.1, max=10, step=0.1, description='$\\omega$', continuous_update=False)
		tfunc = tSin
		sfunc = sSin

	elif func=='Cos':
		var_sldr=widgets.FloatSlider(1, min=0.1, max=10, step=0.1, description='$\\omega$', continuous_update=False)
		tfunc = tCos
		sfunc = sCos

	elif func=='exp':
		var_sldr=widgets.FloatSlider(1, min=-2, max=2, step=0.1, description='$a$', continuous_update=False)
		tfunc = tExp
		sfunc = sExp

	display(widgets.VBox([var_sldr, widgets.interactive_output(computeTransform, {
		't':widgets.fixed(t),
		's':widgets.fixed(s),
		'slim':widgets.fixed(slim),
		'tlim':widgets.fixed(tlim),
		'var':var_sldr,
		'tfunc':widgets.fixed(tfunc),
		'sfunc':widgets.fixed(sfunc),
		'tShift':widgets.fixed(tShift),
		'sShift':widgets.fixed(sShift),
		'fslim':widgets.fixed(fslim)
		})]))

def plotLaplace(t, s, ft, fs, tlim, slim, fslim):
	"""
		Main plotting function for laplace demo

		----------

		Parameters

		----------

		t: array_like
			time samples

		s: array_like
			s samples

		ft: array_like
			points in t space

		fs: array_like
			points in s space

		tlim: array_like
			pair of points for t limits

		slim: array_like
			pair of points for s limits

		fslim: array_like
			s domain y lim
	"""

	_, (tAx, sAx) = plt.subplots(1, 2, figsize=(16,8))

	tAx.plot(t, ft)
	tAx.set_xlim(*tlim)
	tAx.set_title('t Domain', fontsize=20)
	tAx.set_xlabel('t', fontsize=16)
	tAx.set_ylabel('f(t)', fontsize=16)

	sAx.plot(s, fs)
	sAx.set_title('s Domain', fontsize=20)
	sAx.set_xlabel('s', fontsize=16)
	sAx.set_xlim(*slim)
	sAx.set_ylim(*fslim)
	sAx.set_ylabel('F(s)', fontsize=16)

	plt.show()

##########

# ode_suspension

##########

def Suspension():
	"""
		Suspension function to be called from notebook.
	"""
	c_sldr = widgets.FloatSlider(3, min=0, max=3, step=0.1, description='c', continuous_update=False)
	m_sldr = widgets.FloatSlider(1, min=0.1, max=3, step=0.1, description='m', continuous_update=False)
	k_sldr = widgets.FloatSlider(1, min=0.1, max=3, step=0.1, description='k', continuous_update=False)

	return (
		widgets.VBox([
			widgets.HBox([
				c_sldr,
				m_sldr,
				k_sldr
			]),
			widgets.interactive_output(suspensionMain, {'c':c_sldr, 'm':m_sldr, 'k':k_sldr})
			])
	)

def suspensionDE(Y, t, m, c, k):
	"""
		Colin Simpson's code...
		But the beautiful docstring is all mine

		Y: array_like
			contains initial conditions

		t: array_like
			time values

		m: float
			mass parameter

		c: float
			damping coeffcient parameter

		k: float
			spring constant parameter
	"""

	return [Y[1], -c*Y[1]-k*Y[0]/m]

def suspensionMain(c, m, k):

	"""
		Built on code from Colin Simpson.
		Main code for computing suspension behaviour and returning values for plotting.

		m: float
			mass parameter

		c: float
			damping coeffcient parameter

		k: float
			spring constant parameter
	"""

	tmin = 0
	tmax = 15
	t = np.arange(tmin, tmax, 0.1)

	# start at y = 1, dydt = 0
	init = [1, 0]

	y = odeint(suspensionDE, init , t, args=(m, c, k))

	if c*c > 4*m*k:
		title = 'y vs t: over-damping'
	elif c*c == 4*m*k:
		title = 'y vs t: critical-damping'
	elif c == 0:
		title = 'y vs t: no-damping'
	else:
		title = 'y vs t: under-damping'

	plotSuspension(y, t, title, tmin, tmax)

def plotSuspension(y, t, title, tmin, tmax):
	"""
		Main function for plotting suspension output

		y: array_like
			vertical displacement

		t: array_like
			time values

		title: string
			graph title

		tmin: float
			minimum time value

		tmax: float maximum time value
	"""

	_ ,ax = plt.subplots(1,1, figsize=(16,8))
	plt.xlabel('t')
	plt.ylabel('y')
	ax.plot(t,y[:,0], color='C0')
	ax.set_title(title, fontsize=20)
	ax.set_ylim(-1.2, 1.2)
	ax.set_xlim(tmin, tmax)

	#ax.plot([t_i],y[1,0],marker='o', markersize=3, color="red")
	plt.show()

##########

# pendulum

##########

def dvdt(a, t, *args):
	"""
		1st equation of system of ODEs

		----------

		Parameters

		----------

		a: float
			angle

		t: float (unused)
			time

		args[0]: g
			acceleration due to gravity

		args[1]: l
			length of pendulum

		----------

		Returns

		----------

		First DE od ode system
	"""
	g, l = args
	return (g/l)*np.sin(a)

def dadt(v, t, *args):
	"""
		1st equation of system of ODEs

		----------

		Parameters

		----------

		a: float
			angle

		t: float (unused)
			time

		----------

		Returns

		----------

		Second DE of ode system
	"""
	return v

def pendUpdate(i, aVec, vVec, line):
	"""
		Update function for animation

		----------

		Parameters

		----------

		i: int
			animation frame number

		aVec: array_like
			a single value. Don't ask why it's an array not just a float

		vVec: array_like
			v

		line: matplotlib object

		----------

		Returns

		----------

		line: matplotlib object
			line updated with new points

	"""
	h = 0.05
	vVec[0] = runge_step(dvdt, aVec[0], vVec[0], h, 9.81, 1)
	aVec[0] = runge_step(dadt, vVec[0], aVec[0], h)
	# print(vVec)
	line.set_data([0, np.sin(aVec)], [0, np.cos(aVec)])
	return line,

def Pendulum(a0=0, v0=0):
	"""
		Main function for calling matplotlib animation

		a0: float
			starting angle (rad)

		v0: float
			starting angular velocity
	"""
	aVec = [a0]
	vVec = [v0]

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

	line, = ax.plot([0, np.sin(aVec[0])], [0, np.cos(aVec[0])])

	ax.set_xlim(-1.2,1.2)
	ax.set_ylim(-1.2,1.2)
	# frames = 1000 slow to load but good play speed for notebook
	# interval = 5
	anim = animation.FuncAnimation(fig, pendUpdate, frames=150, interval=50, fargs=[aVec, vVec, line], repeat=True, blit=True)

	if __name__=="__main__":
		plt.show()
	else:
		return HTML(anim.to_jshtml())

def pendMain():
	"""
		Main pendulum function called by notebook
	"""
	methods = {'Runge':runge_step, 'Euler':euler_step, 'Improved':improved_euler_step}

	method_drop = widgets.Dropdown(options=methods)

	display(
		method_drop,
		widgets.interactive_output(Pendulum, {})
	)

if __name__=="__main__":
	# pendMain()
	Pendulum()


"""
  Name     : c12_15_simulate_black_Scholes_call.py
  Book     : Python for Finance (2nd ed.)
  Publisher: Packt Publishing Ltd.
  Author   : Yuxing Yan
  Date     : 6/6/2017
  email    : yany@canisius.edu
             paulyxy@hotmail.com
"""
import scipy as sp
from scipy import zeros, sqrt, shape
#
S0 = 40.              # stock price at time zero
X= 40.                # exercise price
T =0.5                # years
r =0.05               # risk-free rate
sigma = 0.2           # annualized volatility
n_steps=100.          # number of steps
#
sp.random.seed(12345) # fix those random numbers
n_simulation = 5000   # number of simulation
dt =T/n_steps
call = sp.zeros([n_simulation], dtype=float)
x = range(0, int(n_steps), 1)
for j in range(0, n_simulation):
    sT=S0
    for i in x[:-1]:
        e=sp.random.normal()
        sT*=sp.exp((r-0.5*sigma*sigma)*dt+sigma*e*sqrt(dt))
        call[j]=max(sT-X,0)
#
call_price=sp.mean(call)*sp.exp(-r*T)
print('call price = ', round(call_price,3))


"""
    Authors: Artur Przybysz (s202384), Elena Mongelli(s181214), Ivan Knezevic (s202386)
"""

# Exercise c

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

# U(x) = e^x;
# U_hat(x) = u_hat1 * N1 + u_hat2 * N2 + u_hat3 * N3

def u_x(x):
    return math.exp(x)

def N1(x, h1):
    if 0 <= x <= h1:
        return (h1 - x) / h1
    else:
        return 0

def N2(x, h1, h2):
    if 0 <= x <= h1:
        return x / h1
    if h1 <= x <= h1 + h2:
        return (h1 + h2 - x) / h2
    else:
        return 0

def N3(x, h1, h2):
    if h1 <= x <= h1 + h2:
        return (x - h1) / h2
    else:
        return 0

def u_hat(x, h1, h2, u_hat1, u_hat2, u_hat3):
    return u_hat1 * N1(x, h1) + u_hat2 * N2(x, h1, h2) + u_hat3 * N3(x, h1, h2)

def u_i(x, h1, h2):
    return u_x(0) * N1(x, h1) + u_x(h1) * N2(x, h1, h2) + u_x(h1 + h2) * N3(x, h1, h2)

if __name__ == '__main__':
    u_hat1 = 1
    u_hat2 = 2.6216
    u_hat3 = math.exp(2)
    h1 = 1
    h2 = 1

    u_hat_values = []
    u_x_values = []
    u_i_values = []
    x = np.arange(0, 2.1, 0.1)
    for i in x:
        u_hat_values.append(u_hat(i, h1, h2, u_hat1, u_hat2, u_hat3))
        u_x_values.append(u_x(i))
        u_i_values.append(u_i(i, h1, h2))

    plt.plot(x, u_hat_values, label="u_hat")
    plt.plot(x, u_x_values, label="u_x")
    plt.plot(x, u_i_values, label="u_i")
    plt.legend()
    plt.show()

    # Exercise d: redefing h1, h2 and u_hat2 since all the other values don't change

    h1 = 4 / 3
    h2 = 2 / 3
    u_hat2 = 3.6996

    u_hat_values = []
    u_x_values = []
    u_i_values = []
    x = np.arange(0, 2.1, 0.1)
    for i in x:
        u_hat_values.append(u_hat(i, h1, h2, u_hat1, u_hat2, u_hat3))
        u_x_values.append(u_x(i))
        u_i_values.append(u_i(i, h1, h2))

    plt.plot(x, u_hat_values, label="u_hat")
    plt.plot(x, u_x_values, label="u_x")
    plt.plot(x, u_i_values, label="u_i")
    plt.legend()
    plt.show()


import numpy as np
import matplotlib.pyplot as plt
from scipy.special import comb

def hypergeom_pmf(N, A, n, x):

    '''
    Probability Mass Function for Hypergeometric Distribution
    :param N: population size
    :param A: total number of desired items in N
    :param n: number of draws made from N
    :param x: number of desired items in our draw of n items
    :returns: PMF computed at x
    '''
    Achoosex = comb(A,x)
    NAchoosenx = comb(N-A, n-x)
    Nchoosen = comb(N,n)

    return (Achoosex)*NAchoosenx/Nchoosen

def hypergeom_cdf(N, A, n, t, min_value=None):

    '''
    Cumulative Density Funtion for Hypergeometric Distribution
    :param N: population size
    :param A: total number of desired items in N
    :param n: number of draws made from N
    :param t: number of desired items in our draw of n items up to t
    :returns: CDF computed up to t
    '''
    if min_value:
        return np.sum([hypergeom_pmf(N, A, n, x) for x in range(min_value, t+1)])

    return np.sum([hypergeom_pmf(N, A, n, x) for x in range(t+1)])

def hypergeom_plot(N, A, n):

    '''
    Visualization of Hypergeometric Distribution for given parameters
    :param N: population size
    :param A: total number of desired items in N
    :param n: number of draws made from N
    :returns: Plot of Hypergeometric Distribution for given parameters
    '''

    x = np.arange(0, n+1)
    y = [hypergeom_pmf(N, A, n, x) for x in range(n+1)]
    plt.plot(x, y, 'bo')
    plt.vlines(x, 0, y, lw=2)
    plt.xlabel('# of desired items in our draw')
    plt.ylabel('Probablities')
    plt.title('Hypergeometric Distribution Plot')
    plt.show()


import numpy as np

def corr_coef(x,y):
    '''It returns the correlation coefficient for two given datasets'''

    x_mean = np.mean(x)
    y_mean = np.mean(y)

    numerator = np.sum((x - x_mean) * (y - y_mean))
    denominator = np.sqrt(np.sum((x - x_mean)*(x - x_mean))) * np.sqrt(np.sum((y - y_mean)*(y - y_mean)))

    r = numerator / denominator

    return r


import numpy as np
import matplotlib.pylab as plt

x = np.array([[1],
              [2],
              [3]])

y = np.array([[4],
              [5],
              [6]])

print(x.T@y)

""" Exercise 2.2.1 """
p = np.array([100, 80, 50]).reshape((-1,1))
n = np.array([3, 4, 5]).reshape((-1,1))

w = p.T@n

print("w: {}".format(w))

print("=============================")

from sklearn.datasets import load_digits, load_iris
import matplotlib.gridspec as gridspec

digits = load_digits()
d1 = digits.images[0]
d2 = digits.images[10]
d3 = digits.images[1]
d4 = digits.images[11]
v1 = d1.reshape(64,1)
v2 = d2.reshape(64,1)
v3 = d3.reshape(64,1)
v4 = d4.reshape(64,1)

print("similarity b/w zeros: ", v1.T@v2)
print("similarity b/w ones: ", v3.T@v4)
print("similarity b/w zero and one: ", v1.T@v3)

""" Exercise 2.2.3 """
X = load_digits().data
first_image = digits.images[0].reshape(64,1)
print(first_image.T@X[0]) #1

#2: Similarities of all images
SoAI = np.dot(X, X.T)

X = load_iris().data
print(X[:,0].mean())

from sklearn.datasets import fetch_olivetti_faces

faces = fetch_olivetti_faces()

f, ax = plt.subplots(1,3)

ax[0].imshow(faces.images[6], cmap=plt.cm.bone)
ax[0].grid(False)
ax[0].set_xticks([])
ax[0].set_yticks([])
ax[0].set_title('image 1: $x_1$')

ax[1].imshow(faces.images[10], cmap=plt.cm.bone)
ax[1].grid(False)
ax[1].set_xticks([])
ax[1].set_yticks([])
ax[1].set_title('image 1: $x_2$')

new_face = 0.7 * faces.images[6] + 0.3*faces.images[10]
ax[2].imshow(new_face, cmap=plt.cm.bone)
ax[2].grid(False)
ax[2].set_xticks([])
ax[2].set_yticks([])
ax[2].set_title('image 3: $x_new$')

# plt.show()

""" Exercise 2.4.6 """
from sklearn.datasets import load_boston
boston = load_boston()
X = boston.data
y = boston.target


"""
Locally Weighted Linear Regression (Loess)

see:
https://xavierbourretsicotte.github.io/loess.html
"""

import os
import sys

from math import ceil
import numpy as np
from scipy import linalg

import matplotlib.pyplot as plt
import pandas as pd
##from IPython.display import Image
##from IPython.display import display
#plt.style.use('seaborn-white')
## if jupyter: %matplotlib inline

import statsmodels.api as sm

#Defining the bell shaped kernel function - used for plotting later on
def kernel_function(xi,x0,tau= .005):
    """compute bell kernel"""
    return np.exp( - (xi - x0)**2/(2*tau)   )

def lowess_bell_shape_kern(x, y, tau = .005):
    """lowess_bell_shape_kern(x, y, tau = .005) -> yest
    Locally weighted regression: fits a nonparametric regression curve to a scatterplot.
    The arrays x and y contain an equal number of elements; each pair
    (x[i], y[i]) defines a data point in the scatterplot. The function returns
    the estimated (smooth) values of y.
    The kernel function is the bell shaped function with parameter tau. Larger tau will result in a
    smoother curve.
    """
    n = len(x)
    yest = np.zeros(n)

    #Initializing all weights from the bell shape kernel function
    w = np.array([np.exp(- (x - x[i])**2/(2*tau)) for i in range(n)])

    #Looping through all x-points
    for i in range(n):
        weights = w[:, i]
        b = np.array([np.sum(weights * y), np.sum(weights * y * x)])
        A = np.array([[np.sum(weights), np.sum(weights * x)],
                      [np.sum(weights * x), np.sum(weights * x * x)]])
        theta = linalg.solve(A, b)
        yest[i] = theta[0] + theta[1] * x[i]

    return yest

def lowess_ag(x, y, f=2. / 3., iter=3):
    """lowess(x, y, f=2./3., iter=3) -> yest
    Lowess smoother: Robust locally weighted regression.
    The lowess function fits a nonparametric regression curve to a scatterplot.
    The arrays x and y contain an equal number of elements; each pair
    (x[i], y[i]) defines a data point in the scatterplot. The function returns
    the estimated (smooth) values of y.
    The smoothing span is given by f. A larger value for f will result in a
    smoother curve. The number of robustifying iterations is given by iter. The
    function will run faster with a smaller number of iterations.
    """
    n = len(x)
    r = int(ceil(f * n))
    h = [np.sort(np.abs(x - x[i]))[r] for i in range(n)]
    w = np.clip(np.abs((x[:, None] - x[None, :]) / h), 0.0, 1.0)
    w = (1 - w ** 3) ** 3
    yest = np.zeros(n)
    delta = np.ones(n)

    for iteration in range(iter):
        for i in range(n):
            weights = delta * w[:, i]
            b = np.array([np.sum(weights * y), np.sum(weights * y * x)])
            A = np.array([[np.sum(weights), np.sum(weights * x)],
                          [np.sum(weights * x), np.sum(weights * x * x)]])
            beta = linalg.solve(A, b)
            yest[i] = beta[0] + beta[1] * x[i]

            residuals = y - yest
            s = np.median(np.abs(residuals))
            delta = np.clip(residuals / (6.0 * s), -1, 1)
            delta = (1 - delta ** 2) ** 2

    return yest

def lowess_sm(x, y, f=1. / 3., iter=3):
    """
    ref:
    https://www.statsmodels.org/stable/generated/statsmodels.nonparametric.smoothers_lowess.lowess.html
    """
    lowess_sm_ = sm.nonparametric.lowess
    return lowess_sm_(y, x, f, iter, return_sorted = False)

def smooth(mrun, key, inplace, tau, debug, show, input_file):
    """
    Smooth key column data from mrun dataset
    """

    filteredkey = "filtered%s" % key
    df = mrun.getData()
    Meanval = df[key].mean()

    #Initializing noisy non linear data
    x = df["t"].to_numpy()
    y = df[key].to_numpy()

    if debug:
        plt.figure(figsize=(10,6))
        plt.scatter(x,y, facecolors = 'none', edgecolor = 'darkblue', label = key)

    yest_bell = lowess_bell_shape_kern(x,y,tau)
    if debug:
        plt.plot(x,yest_bell,color = 'red', label = 'Loess: bell shape kernel')
        x0 = (x[0] + x[40])/2.
        plt.fill(x[:40],Meanval*kernel_function(x[:40],x0,tau), color = 'lime', alpha = .5, label = 'Bell shape kernel')
        if show:
            plt.show()
        else:
            f_extension = os.path.splitext(input_file)[-1]
            imagefile = input_file.replace(f_extension, "-smoothed%s.png" % key)
            print("save to %s" % imagefile)
            plt.savefig(imagefile, dpi=300)
        plt.close()

    if inplace:
        # replace key by filtered ones
        df[filteredkey] = pd.Series(yest_bell)
        del df[key]
        df.rename(columns={filteredkey: key}, inplace=True)
    # except:
    #     print("smooth: bell_shape_kern smoother failed")
    return mrun

def filterpikes(mrun, key, inplace, threshold, twindows, debug, show, input_file):
    """
    # filter spikes
    # see: https://ocefpaf.github.io/python4oceanographers/blog/2015/03/16/outlier_detection/
    """

    print("type(mrun):", type(mrun))
    df = mrun.getData() #.extractData(keys)

    kw = dict(marker='o', linestyle='none', color='r', alpha=0.3)
    mean = df[key].mean()
    # print("%s=" % key, type(df[key]), df[key])

    if mean != 0:
        var = df[key].var()
        # print("mean(%s)=%g" % (key,mean), "std=%g" % math.sqrt(var) )
        filtered = df[key].rolling(window=twindows, center=True).median().fillna(method='bfill').fillna(method='ffill')
        filteredkey = "filtered%s" % key
        # print("** ", filteredkey, type(filtered))

        df = df.assign(**{filteredkey: filtered.values})
        # print("*** ", filteredkey, df[filteredkey])

        difference = np.abs((df[key] - filtered)/mean*100)
        outlier_idx = difference > threshold

        if debug:
            ax = plt.gca()
            df[key].plot()
            # filtered.plot()
            df[filteredkey].plot()
            df[key][outlier_idx].plot(**kw)

            ax.legend()
            plt.grid(b=True)
            plt.title(mrun.getInsert().replace(r"_",r"\_") + ": Filtered %s" % key)
            if show:
                plt.show()
            else:
                f_extension = os.path.splitext(input_file)[-1]
                imagefile = input_file.replace(f_extension, "-filtered%s.png" % key)
                print("save to %s" % imagefile)
                plt.savefig(imagefile, dpi=300)
            plt.close()

        if inplace:
            # replace key by filtered ones
            del df[key]
            df.rename(columns={filteredkey: key}, inplace=True)

    return mrun

def lagged_correlation(df, target, key, t):
    """
    Compute lag correlation between target and key df column
    """

    lagged_correlation = df[target].corr(df[key].shift(+t))
    print("type(lagged_correlation):", type(lagged_correlation))
    print("lagged_correlation(t=%g):" % t, lagged_correlation)

if __name__ == "__main__":

    import argparse
    import python_magnetrun

    parser = argparse.ArgumentParser()
    parser.add_argument("input_file")
    parser.add_argument("--keys", help="specify keys to select (eg: Tin1;Tin2)", default="Tin1")
    parser.add_argument("--show", help="display graphs (requires X11 server active)", action='store_true')
    parser.add_argument("--debug", help="activate debug mode", action='store_true')

    # define subparser: filter, smooth, lag_correlation
    subparsers = parser.add_subparsers(title="commands", dest="command", help='sub-command help')

    parser_filter = subparsers.add_parser('filter', help='filter help')
    parser_smooth = subparsers.add_parser('smooth', help='smooth help')
    parser_lag = subparsers.add_parser('lag', help='lag help')

    parser_filter.add_argument("--threshold", help="specify a threshold for filter", type=float, default=0.5)
    parser_filter.add_argument("--twindows", help="specify a window length", type=int, default=10)

    parser_smooth.add_argument("--method", help="select a smoother for data", type=str, choices=['ag','bell_kernel','statsmodel_sm', 'all'], default='bell_kernel')
    parser_smooth.add_argument("--smooth_params", help="pass param for smoother method (eg \"f;iter\")", type=str, default='400')
    # parser.add_argument("--smoothing_f", help="specify smoothing f param", type=float, default=0.25)
    # parser.add_argument("--smoothing_tau", help="specify smoothing tau param", type=float, default=0.005)
    # parser.add_argument("--smoothing_iter", help="specify smoothing iter param", type=int, default=5)

    parser_lag.add_argument("--target", help="specify a target field", type=str, default='tsb')
    parser_lag.add_argument("--trange", help="specify a range for t", type=int, default=100)

    args = parser.parse_args()

    threshold = 0.5
    twindows = 10
    if args.command == 'filter':
        threshold = args.threshold
        twindows = args.twindows

    smoothing_f = 0.7
    smoothing_tau = 400
    smoothing_iter = 3
    if args.command == 'smooth':
        params = args.smooth_params.split(';')
        if args.method == "ag":
            smoothing_f = float(params[0])
            smoothing_iter = int(params[1])
        elif args.method == "bell_kernel":
            smoothing_tau = float(params[0])
        elif args.method == "statsmodel_sm":
            smoothing_tau = float(params[0])
        else:
            smoothing_tau = float(params[0])
            smoothing_f = float(params[1])
            smoothing_iter = int(params[2])

    f_extension=os.path.splitext(args.input_file)[-1]
    if f_extension != ".txt":
        print("so far only txt file support is implemented")
        sys.exit(0)

    filename = os.path.basename(args.input_file)
    result = filename.startswith("M")
    if result:
        try:
            index = filename.index("_")
            args.site = filename[0:index]
            print("site detected: %s" % args.site)
        except:
            print("no site detected - use args.site argument instead")
            pass
    mrun = python_magnetrun.MagnetRun.fromtxt(args.site, args.input_file)
    mrun.getMData().addTime()
    start_timestamp = mrun.getMData().getStartDate()
    dkeys = mrun.getKeys()

    inplace = False
    skeys = args.keys.split(";")
    if args.command == 'filter':
        for key in skeys:
            filterpikes(mrun, key, inplace, threshold, twindows, args.debug, args.show, args.input_file)

    if args.command == 'smooth':
        for key in skeys:
            selected_df = mrun.getMData().extractData(["t", key])
            Meanval = selected_df[key].mean()

            #Initializing noisy non linear data
            x = selected_df["t"].to_numpy() #np.linspace(0,1,100)
            y = selected_df[key].to_numpy() # np.sin(x * 1.5 * np.pi )

            print("display Weighted Linear Regression")
            plt.figure(figsize=(10,6))
            plt.scatter(x,y, facecolors = 'none', edgecolor = 'darkblue', label = key)

            #
            print("compute Locally Weighted Linear Regression %s" % args.method)
            if args.method == "ag":
                try:
                    yest = lowess_ag(x, y, f=smoothing_f, iter=smoothing_iter)
                    plt.plot(x,yest, color = 'orange', label = 'Loess: A. Gramfort')
                except:
                    print("Failed to build lowess_ag")

            if args.method == "bell_kernel":
                try:
                    yest_bell = lowess_bell_shape_kern(x,y,smoothing_tau)
                    if args.debug:
                        x0 = (x[0] + x[40])/2.
                        plt.fill(x[:40],Meanval*kernel_function(x[:40],x0,args.smoothing_tau), color = 'lime', alpha = .5, label = 'Bell shape kernel')
                        plt.plot(x,yest_bell,color = 'red', label = 'Loess: bell shape kernel')
                except:
                    print("Failed to build bell")

            if args.method == "statsmodel_sm":
                try:
                    # f = 0.7 # between 0 and 1, by default: 2./3.
                    yest_sm = lowess_sm(x,y, f=smoothing_f, iter=smoothing_iter)
                    plt.plot(x,yest_sm, color = 'magenta', label = 'Loess: statsmodel') # marker="o",
                except:
                    print("Failed to build sm")

            plt.legend()
            plt.title('Loess regression comparisons')
            if args.show:
                plt.show()
            else:
                imagefile = mrun.getSite()
                start_date = ""
                start_time = ""
                if "Date" in dkeys and "Time" in dkeys:
                    tformat="%Y.%m.%d %H:%M:%S"
                    start_date=mrun.getMData().getData("Date").iloc[0]
                    start_time=mrun.getMData().getData("Time").iloc[0]

                plt.savefig('%s_%s---%s-smoothed-%s.png' % (imagefile,str(start_date),str(start_time),key) , dpi=300 )
            plt.close()

    if args.command == 'lag':
        for key in skeys:
            df = mrun.getData()
            for t in range(args.trange):
                lagged_correlation(df, args.target, key, t)

import numpy,from scipy,import statsmodels


import math
import random

import sympy as sym
import numpy as np
from scipy.optimize import brentq

r, s, x, theta_r, tau = sym.symbols("r, s, x, theta_r, tau", positive=True)

alpha, H, beta, Gamma, F, epsilon = sym.symbols(
    "alpha, H, beta, Gamma, F, epsilon", positive=True
)

def theta(r, s, theta_r=theta_r):
    """
    Return the quantity of horn gained:

    Parameters
    ----------

    s: float between 0 and 1 - the proportion of poachers acting selectively
    r: float between 0 and 1 - the proportion of horns devalued
    theta_r: float between 0 and 1 - the value of dehorned horn
    """
    return s * (1 - r) + (1 - s) * ((theta_r - 1) * r + 1)

def selective_time(r, tau):
    """
    Expected time spent in park by a selective poacher

    Parameters
    ----------

    r: float between 0 and 1 - the proportion of horns devalued
    tau: float between       - the time taken to kill a horn
    """
    return (r + tau * (1 - r)) / (1 - r)

def indiscriminate_time(r, tau, N_r):
    """
    Expected time spent in park by an indiscriminate poacher

    Parameters
    ----------

    r: float between 0 and 1       - the proportion of horns devalued
    tau: float                     - the time taken to kill a horn
    N_r: int                       - number of devalued rhinos that correspond
                                     to a single valued rhino
    """
    return tau * (1 - r ** N_r) / (1 - r)

def gain(s=s, x=x, H=H, r=r, theta_r=theta_r, alpha=alpha):
    """
    Return the total gain of the poachers based on their behaviour.
    """
    return theta(r, s, theta_r) * H * theta(r, x, theta_r) ** (-alpha)

def cost(s=s, tau=tau, x=x, r=r, F=F, beta=beta, theta_r=theta_r):

    N_r = sym.ceiling(1 / theta_r)
    return (
        F
        * (1 - r) ** beta
        * (
            s * selective_time(r=r, tau=tau)
            + (1 - s) * indiscriminate_time(r=r, tau=tau, N_r=N_r)
        )
    )

def utility(
    s=s, x=x, F=F, H=H, r=r, alpha=alpha, beta=beta, tau=tau, theta_r=theta_r,
    Gamma=Gamma
):
    """
    The total utility for a strategy
    sigma=(s, 1-s) in a population chi=(x, 1-x)
    """
    return gain(s=s, x=x, H=H, r=r, theta_r=theta_r, alpha=alpha) - cost(
        s=s, tau=tau, x=x, r=r, F=F, beta=beta, theta_r=theta_r
    ) - (1 - s) * Gamma