'''
Collated by Ching-Shih Tsou 鄒慶士 博士 (Ph.D.) Distinguished Prof. at the Department of Mechanical Engineering/Director at the Center of Artificial Intelligence & Data Science (機械工程系特聘教授兼人工智慧暨資料科學研究中心主任), MCUT (明志科技大學); Prof. at the Institute of Information & Decision Sciences (資訊與決策科學研究所教授), NTUB (國立臺北商業大學); the Chinese Academy of R Software (CARS) (中華R軟體學會創會理事長); the Data Science and Business Applications Association of Taiwan (DSBA) (臺灣資料科學與商業應用協會創會理事長); the Chinese Association for Quality Assessment and Evaluation (CAQAE) (中華品質評鑑協會常務監事); the Chinese Society of Quality (CSQ) (中華民國品質學會大數據品質應用委員會主任委員
Notes: This code is provided without warranty.
'''

### 繪製時域波形訊號圖
# 現在給定三個 sine 波，頻率freq與振幅amp分別為 (20, 12), (100, 5), (250, 2)，給定兩秒內取 2000 個點(每秒取樣率為 1000 Hz)後將結果繪製出來。

import numpy as np
import matplotlib.pyplot as plt
from scipy import pi

#%matplotlib inline

sample_num = 2000 # Sampling points 取樣總點數
total_time = 2 # Sampling period 取樣期間
sampling_rate = sample_num / total_time # 取樣頻率為 1000 Hz(單位時間的取樣點數為1000點)

fs = [(20, 12), (100, 5), (250, 2)] # sin 波的頻率與振幅組合。 (Hz, Amp) 頻率越來越高，振幅越來越小！
noise_mag = 2 # 雜訊變異數

time = np.linspace(0, total_time, sample_num, endpoint=False) # endpoint=False：不含右端點

vib_data = [amp * np.sin(2*pi*hz*time) for hz, amp in fs] # 公式：A*sin(2*pi*w*t)

max_time = int(sample_num / 4) # 繪圖用，只畫前500(=2000/4)點
# max_time = sample_num # 如果要畫全部的點

# 請一路全選到第二個plt.ylim((-24, 24))後執行
plt.figure(figsize=(12, 8))
# 繪製個別訊號 Show seperated signal
for idx, vib in enumerate(vib_data): # 0~2 三組正弦波
    plt.subplot(2, 2, idx+1) # 繪製idx+1個圖
    plt.plot(time[0:max_time], vib[0:max_time])
    plt.xlabel('time')
    plt.ylabel('vib_' + str(idx))
    plt.ylim((-24, 24)) # 此處先寫死

# vib加總前面三個訊號(20Hz, 100Hz, 250Hz)，並再加上雜訊
vib = sum(vib_data) + np.random.normal(0, noise_mag, sample_num) # Add noise (雜訊是從Normal(0, noise_mag = 2)中隨機取樣)
# sum(vib_data).shape # (2000,)

plt.subplot(2, 2, 4)
plt.plot(time[0:max_time], vib[0:max_time])
plt.xlabel('time')
plt.ylabel('vib(with noise)')
plt.ylim((-24, 24))

# 其中
# 左上 - 20 Hz 訊號
# 右上 - 100 Hz 訊號
# 左下 - 250 Hz 訊號
# 右下 - 前三個訊號的加總並加入雜訊的結果。

### 時域轉頻域的快速傅立葉轉換(FFT)
# 取樣時間T_{d}內取N個點，兩者相除為取樣間隔時間\delta_{t}
# T_{d}/N=\delta_{t}
# 取樣間隔時間\delta_{t}的倒數為取樣頻率F_{s}
# F_{s}=1/\delta_{t}

# 當透過FFT將資料轉換到N點的\hat{f_{n}}後，各點的間隔頻率為\delta_{F}
# \delta_{F}=F_{s}/N

# 每個點對應到的頻率為
# F_{n}=n \delta_{F}, n=0,1,...N-1

# Nyquist Frequency: 超過F_{s}/2會產生混疊(aliasing)的現象。因此 FFT 轉換後的\hat{f_{n}}只有在 0≤n≤N/2 的範圍內有意義

# 載入傅立葉轉換模組
from scipy.fftpack import fft

fd = np.linspace(start=0.0, stop=sampling_rate, num=int(sample_num), endpoint=False) # 0到1000的區間產生2000個頻率點

# 總和訊號傅立葉轉換
vib_fft = fft(vib) # Array of Complex128 (2000,) 有實有虛了！與vib維數相同
mag = 2/sample_num * np.abs(vib_fft) # Magnitude (再確認一下此公式！Since the calculated FFT will be symmetric such that the magnitude of only the first half points are unique (and the rest are symmetrically redundant), the main idea is to display only half of the FFT spectrum. )

plt.plot(fd[0:int(sample_num/2)], mag[0:int(sample_num/2)])
plt.xlabel('Hz')
plt.ylabel('Mag')

# 從上圖可以看出，在 20 Hz, 100 Hz 以及 250 Hz 的位置各有一個明顯的能量高峰，資料量與數值與之前設定的頻率一致，以此可知 FFT 可幫助我們分解找出原始訊號中的訊號組合。

### 逆快速傅立葉轉換(Inverse FFT)再轉回時域訊號
from scipy.fftpack import ifft

vib_back = ifft(vib_fft) # ifft結果vib_back(振幅 + 相位角j)，也是Array of Complex128 (2000,)

vib_re = np.real(ifft(vib_fft)) # 取實部(振幅) Real part of complex number

vib_im = np.imag(ifft(vib_fft)) # 取虛部(相位角)

plt.plot(time[0:max_time], vib_re[0:max_time]) # 轉換回來的訊號幾乎相等於原始訊號，為何只繪實部？Ans.實部是原時域訊號值
plt.ylim((-24, 24))

# # np.abs()計算振幅
# mag = np.abs(ifft(vib_fft))

# plt.plot(time[0:max_time], mag[0:max_time])
# plt.ylim((0, 24))

### 總結 (可以先看這裡)
# Fourier Series -> Complex Fourier Series (Euler's Formula) -> (因為取樣) Discrete Time Fourier Transform (矩陣形式, 反矩陣) -> Fast Fourier Transform (time complexity from O(N^2) to O(NlogN)) -> (與程式碼相關) T/N = delta_t, F = N/T = 1/delta_t -> delta_F = F/N = 1/T (N點間隔頻率), F_n = n delta_F, n = 0, 1, ..., N-1 -- Nyquist Frequency --> F/2 for aliasing 0 <= n <= N/2

### 參考資料
# 從傅立葉級數到快速傅立葉轉換 http://blog.yeshuanova.com/2019/04/fft_intro/


import numpy as np
import plot

class RBF:
    def __init__(self, mu, sig_squared):
        self.position = mu
        self.variance = sig_squared
    def phi(self, x):
        distance = np.absolute(x - self.position)**2
        return np.exp(-distance/(2*self.variance))

def run(weights, rbfs, x):
    return sum(w*rbf.phi(x) for w,rbf in zip(weights, rbfs))

def least_squares(data, values, N = None):
    if N == None:
        N = len(data)
    mu = np.random.choice(data, N)
    sig = [min(a - b for b in data if a != b) for a in data]
    rbfs = [RBF(m, s) for m, s in zip(mu, sig)]
    phi = np.array([[rbf.phi(x) for rbf in rbfs] for x in data])
    w = np.linalg.solve(phi, values)
    return w, rbfs

def iterative_batch(train_data, test_data, train_values, test_values, error=0.1, N = None):
    if N == None:
        N = len(train_data)
    w = np.random.normal(-1, 1, N)
    mu = np.random.choice(train_data, N)
    sig = [min(a - b for b in train_data if a != b)**2 for a in train_data]
    rbfs = [RBF(m, s) for m, s in zip(mu, sig)]
    big_phi = np.array([[rbf.phi(x) for rbf in rbfs] for x in train_data])

    w = np.linalg.lstsq(big_phi.T.dot(big_phi), big_phi.T.dot(train_values), rcond=-1)[0]

    # Test data error
    big_phi = np.array([[rbf.phi(x) for rbf in rbfs] for x in test_data])
    total_error =  sum((big_phi.dot(w) - test_values) ** 2)/len(test_data)
    return w, rbfs, total_error

def least_squares_sin_noise(N=5, shouldPlot=True):
    train_data = list(np.arange(0, 2*np.pi, 0.1))
    test_data = list(np.arange(0.05, 2*np.pi, 0.1))
    train_noise = np.random.normal(0,0.1,len(train_data))
    test_noise = np.random.normal(0,0.1,len(test_data))
    train_values = np.array([np.sin(2*x) for x in train_data])
    test_values = np.array([np.sin(2*x) for x in test_data])
    train_values += train_noise
    test_values += test_noise
    w, rbfs, error = iterative_batch(train_data, test_data, train_values, test_values, N=N)
    result = [run(w, rbfs, x) for x in test_data]
    if shouldPlot:
        plot.plot(train_data, train_values, [-1, 1])
        plot.plot(test_data, result, [-1, 1])
    return error

def least_squares_square(N=5, shouldPlot=True):
    train_data = list(np.arange(0, 2*np.pi, 0.1))
    test_data = list(np.arange(0.05, 2*np.pi, 0.1))
    train_values = np.array([1 if x < np.pi/2 or (x>np.pi and x<3*np.pi/2) else -1 for x in train_data])
    test_values = np.array([1 if x < np.pi/2 or (x>np.pi and x<3*np.pi/2) else -1 for x in test_data])
    w, rbfs, error = iterative_batch(train_data, test_data, train_values, test_values, N=N)
    result = [run(w, rbfs, x) for x in test_data]
    if shouldPlot:
        plot.plot(train_data, train_values, [-1, 1])
        plot.plot(test_data, result, [-1, 1])
    return error

def least_squares_square_modified(N=5, shouldPlot=True):
    train_data = list(np.arange(0, 2*np.pi, 0.1))
    test_data = list(np.arange(0.05, 2*np.pi, 0.1))
    train_values = np.array([1 if x < np.pi/2 or (x>np.pi and x<3*np.pi/2) else -1 for x in train_data])
    test_values = np.array([1 if x < np.pi/2 or (x>np.pi and x<3*np.pi/2) else -1 for x in test_data])
    w, rbfs, error = iterative_batch(train_data, test_data, train_values, test_values, N=N)
    result = [run(w, rbfs, x) for x in test_data]
    result = [1 if x>0 else -1 for x in result]
    error =  sum((result - test_values) ** 2)/len(test_data)
    if shouldPlot:
        plot.plot(train_data, train_values, [-1, 1])
        plot.plot(test_data, result, [-1, 1])
    return error

def least_squares_sin(N=5, shouldPlot=True):
    train_data = list(np.arange(0, 2*np.pi, 0.1))
    test_data = list(np.arange(0.05, 2*np.pi, 0.1))
    train_values = np.array([np.sin(2*x) for x in train_data])
    test_values = np.array([np.sin(2*x) for x in test_data])
    w, rbfs, error = iterative_batch(train_data, test_data, train_values, test_values, N=N)
    result = [run(w, rbfs, x) for x in test_data]
    if shouldPlot:
        plot.plot(train_data, train_values, [-1, 1])
        plot.plot(test_data, result, [-1, 1])
    return error

if __name__ == "__main__":
    num = 100
    test_nodes = [_ for _ in range(1, num)]
    result = []
    for n in test_nodes:
        #result.append(least_squares_sin(n, False))
        #result.append(least_squares_square(n, False))
        result.append(least_squares_sin_noise(n, False))
        #result.append(least_squares_square_modified(n, False))

    plot.plot(test_nodes, result, [0, 1])


###############################################
#
# Created by Khushi Agrawal on 05/10/21.
# Copyright (c) 2021 Khushi Agrawal. All rights reserved.
#
# Lab Program 1
# Contents: Perform grey-level transformation
#   - Negative Image Transformation
#   - Logarithmic Image Transformation
#   - Power Law Transformation
#
##############################################

import cv2
import numpy as np
import math

# Negative image tranformation
def neg_trans(img):
	height = img.shape[0]
	width = img.shape[1]
	for i in range(height):
		for j in range(width):
			img[i][j] = 255 - int(img[i][j])

	return img

# Logarithmic image transformation
def log_trans(img):
	height = img.shape[0]
	width = img.shape[1]
	val = img.flatten()
	maxv = max(val)
	c = 255/(math.log(1+maxv))
	for i in range(height):
		for j in range(width):
			new_val = int(c * (math.log( 1 + int(img[i][j]))))
			if new_val > 255:
				new_val = 255
			img[i][j] = new_val

	return img

# Power Law Transformation
def powerLawTransform(c,pixel,gamma):
    new_pixel = c*pow(pixel,(1/gamma))
    return round(new_pixel)

def power_law(img, c, gamma):
    height = img.shape[0]
    width = img.shape[1]
    for w in range(width):
        for h in range(height):
            #retrieve the pixel value
            p = int(img[w][h])
            #add value to the pixel
            img[w,h] = powerLawTransform(c,p,gamma)
    return img

# ERROR (Solved): https://stackoverflow.com/questions/52676020/opencv-src-empty-in-function-cvtcolor-error
img = 'images/Aaron_Eckhart_0001.jpg'
image = cv2.imread(img)
gray_img = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv2.imwrite('images/ques_1_gray_image.png', gray_img)

neg_img = neg_trans(gray_img)
cv2.imwrite('images/ques_1_neg_img.png',neg_img)
print("Negative transformed image is saved as ques_1_neg_img.png.")

log_img = log_trans(gray_img)
cv2.imwrite('images/ques_1_log_img.png',log_img)
print("Log-transformation output image is saved as ques_1_log_img.png.")

#The scaling constant c is chosen so that the maximum output value is 255
c = 255/(math.log10(1+ np.amax(gray_img)))
gamma = float(input("Enter a value for gamma : "))

pow_law = power_law(gray_img, c, gamma)
cv2.imwrite('images/ques_1_power_law_img_(y-9).png', pow_law)
print("Power Law Transformation is saved as ques_1_power_law_img.png")


import numpy as np
import scipy

def accuracy(nHit, nMiss, nFA, nCR):
    return (nHit + nCR) / (nHit + nMiss + nFA + nCR)

def d_prime(nHit, nMiss, nFA, nCR):
    hitRate = nHit / (nHit + nMiss)
    faRate  = nFA  / (nFA + nCR)

    zHit = scipy.stats.norm.ppf(hitRate)
    zFA = scipy.stats.norm.ppf(faRate)

    #     hitRateAdjusted = (nHit + 0.5) /((nHit + 0.5) + nMiss + 1)
    #     faRateAdjusted  = (nFA  + 0.5) /((nFA  + 0.5) + nCR + 1)

    #     zHit = scipy.stats.norm.ppf(hitRateAdjusted)
    #     zFA = scipy.stats.norm.ppf(faRateAdjusted)

    return zHit - zFA

# Given number of elements in a subset and total set, return their ratio
# If both are zero, return zero
# Complain if values are inadequate
def rate_0_protected(nArrSub, nArrTot):
    assert ~np.any(nArrSub < 0),       "Only non-negative arrays allowed"
    assert ~np.any(nArrTot < 0),       "Only non-negative arrays allowed"
    assert ~np.any(nArrTot < nArrSub), "Subset can't have greater size than the whole set"

    nonZeroIdx = nArrTot != 0
    rez = np.copy(nArrSub)
    rez[nonZeroIdx] /= nArrTot[nonZeroIdx]
    return rez

def accuracy_indices(yHat, y):
    if yHat.shape == y.shape:
        yReal = y
    elif (yHat.ndim == 2) and (y.ndim == 1) and (yHat.shape[0] == y.shape[0]):
        # If correct output is the same for all tests, extrude it along the second dimensions
        yReal = np.repeat([y], yHat.shape[1], axis=0).T
    else:
        raise ValueError("Unexpected shapes", yHat.shape, y.shape)

    idxsDict = {
        'TP' :  yHat &  yReal,  # True Positives
        'FP' :  yHat & ~yReal,  # False Positives
        'FN' : ~yHat &  yReal,  # False Negatives
        'TN' : ~yHat & ~yReal   # True Negatives
    }

    return yReal, idxsDict

def accuracy_tests(yHat, y, axis=0):
    yReal, idxsDict = accuracy_indices(yHat, y)

    freqTrue     = np.mean(yReal, axis=axis)    # Frequency of true outcomes
    freqHatTrue  = np.mean(yHat, axis=axis)     # Frequency of true predictions
    freqFalse    = 1 - freqTrue                 # Frequency of false outcomes
    freqHatFalse = 1 - freqHatTrue              # Frequency of false predictions

    freqTP = np.mean(idxsDict['TP'], axis=axis)   # Frequency of True Positives
    freqFP = np.mean(idxsDict['FP'], axis=axis)   # Frequency of False Positives
    freqFN = np.mean(idxsDict['FN'], axis=axis)   # Frequency of False Negatives
    freqTN = np.mean(idxsDict['TN'], axis=axis)   # Frequency of True Negatives

    TPR = rate_0_protected(freqTP, freqTrue)   # True Positive Rate - number of TP out of all that were possible
    FPR = rate_0_protected(freqFP, freqFalse)  # False Positive Rate - number of FP out of all that were possible

    return {
        "TP frequency": freqTP,
        "FP frequency": freqFP,
        "FN frequency": freqFN,
        "TN frequency": freqTN,
        "TPR": TPR,
        "FPR": FPR
    }


import math
import time
import cPickle
import argparse
import matplotlib.pyplot as plt
import numpy as np

def check_prime(N):
    sqrtN = math.sqrt(N)
    Nf = float(N)
    for i in xrange(2, int(sqrtN) + 1):
        if (Nf / i).is_integer():
            return False
    return True

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Project description')
    parser.add_argument(
        '--create_data',
        action="store_true",
        default=False,
        help='if present then calculate data, if absent then plot')
    args = parser.parse_args()
    filename = "08_prime_time_cost_1e4to1e6.pickle"

    if args.create_data:
        primes = []
        are_primes = []
        times = []
        # explanation
        #lower_range = 3
        #upper_range = 1000
        # good range
        lower_range = 10001
        upper_range = 1000000
        numbers_to_test = np.array(range(lower_range, upper_range, 10))
        for possible_prime in numbers_to_test:
            min_time = 9999
            # calculate several times so we record the fastest time
            for loop in xrange(20):
                t1 = time.time()
                is_prime = check_prime(possible_prime)
                t2 = time.time()
                min_time = min(t2 - t1, min_time)
            times.append(min_time)
            # we only need to apped the result once
            if is_prime:
                primes.append(possible_prime)
            are_primes.append(is_prime)
        print primes[:10], primes[-10:]
        are_primes = np.array(are_primes)
        times = np.array(times)
        cPickle.dump(
            (numbers_to_test, are_primes, times), open(filename, 'wb'))
    else:
        numbers_to_test, are_primes, times = cPickle.load(open(filename))
        # plot the two items three times, the last doesn't have a label (but is
        # there to over-plot the Not Prime result), so the Legend items are
        # displayed in the correct sequence
        plt.plot(numbers_to_test[are_primes], times[
                 are_primes], 'x', color='k', alpha=1, label="Prime")
        plt.plot(
            numbers_to_test[
                are_primes == False],
            times[
                are_primes == False],
            '.',
            color='b',
            alpha=0.6,
            label="Not prime")
        plt.plot(numbers_to_test[are_primes], times[
                 are_primes], 'x', color='k', alpha=1, )
        plt.xlabel("Integers to test")
        plt.ylabel("Seconds per test")
        plt.title("Time cost for checking primality")
        plt.ylim(0, 0.00015)  # fits neatly on Ian's laptop
        # plt.grid()
        plt.legend()
        plt.savefig("08_prime_time_cost_1e4to1e6.png")
        plt.tight_layout()
        # plt.show()


__author__ = 'htelg'

import numpy as np
from numpy import linalg as la

def cart2spheric(xyz):
    """
    Returns
    -------
    (r,theta, phi):
        r: length
        theta: polar angle (between vector and z axesl)
        rho: azimuth angle (in x,y plane)
    """
    #     sph = np.zeros(xyz.shape)#np.hstack((xyz, np.zeros(xyz.shape)))
    #     xy = xyz[:,0]**2 + xyz[:,1]**2
    #     sph[:,0] = np.sqrt(xy + xyz[:,2]**2)
    #     sph[:,1] = np.arctan2(np.sqrt(xy), xyz[:,2]) # for elevation angle defined from Z-axis down
    #     #ptsnew[:,4] = np.arctan2(xyz[:,2], np.sqrt(xy)) # for elevation angle defined from XY-plane up
    #     sph[:,2] = np.arctan2(xyz[:,1], xyz[:,0])
    if len(xyz.shape) == 1:
        xyz = np.array([xyz])
        d1 = True
    else:
        d1 = False
    rtp = np.zeros(xyz.shape)  # np.hstack((xyz, np.zeros(xyz.shape)))
    rtp[:, 0] = np.sqrt(xyz[:, 0] ** 2 + xyz[:, 1] ** 2 + xyz[:, 2] ** 2)
    rtp[:, 1] = np.arccos(xyz[:, 2] / rtp[:, 0])
    rtp[:, 2] = np.arctan2(xyz[:, 1], xyz[:, 0])
    if d1:
        return rtp[0]
    else:
        return rtp

def spheric2cart(rtp):
    """r,theta,phi"""
    if len(rtp.shape) == 1:
        rtp = np.array([rtp])
        d1 = True
    else:
        d1 = False
    xyz = np.zeros(rtp.shape)
    xyz[:, 0] = rtp[:, 0] * np.sin(rtp[:, 1]) * np.cos(rtp[:, 2])
    xyz[:, 1] = rtp[:, 0] * np.sin(rtp[:, 1]) * np.sin(rtp[:, 2])
    xyz[:, 2] = rtp[:, 0] * np.cos(rtp[:, 1])
    if d1:
        return xyz[0]
    else:
        return xyz

def angleBetweenVectors(a, b):
    """returns the angle between two vectors
    Arguments
    ---------
    a,b: np.array of shape (n,3,) or (3,)

    Returns
    -------
    """
    if a.shape != b.shape:
        raise ValueError('a and be must have the same shape')
    if len(a.shape) == 1:
        return (np.arccos(np.dot(a, b) / (la.norm(a) * la.norm(b))))

    else:
        angles = np.zeros(a.shape[0])
        for e, i in enumerate(a):
            angles[e] = (np.arccos(np.dot(a[e], b[e]) / (la.norm(a[e]) * la.norm(b[e]))))
        return angles


# --------------
# Creation of Calculator

import pandas as pd
import numpy as np
import math

# Define a class called complex_numbers which accepts 2 parameters:

# real: int64, float64, represents real component of the complex number
# imag: int64, float64, represents imaginary component of the complex number

class complex_numbers():
    def __init__(self,real,imag):
        self.real = real
        self.imag = imag

    def __repr__(self):
        if self.real == 0.0 and self.imag == 0.0:
            return "0.00"
        if self.real == 0:
            return "%.2fi" % self.imag
        if self.imag == 0:
            return "%.2f" % self.real
        return "%.2f %s %.2fi" % (self.real, "+" if self.imag >= 0 else "-", abs(self.imag))

# Task 2:

# Define the following operations for the class by implementing operator overloading.

# '+' (complex number addition)

# The sum of two complex numbers C= a+ ib and C'= a' + ib' is

# complex_add

# - Create a function called __add__ which takes two parameters 'self' and 'other'

# - Inside the function, should be statements performing complex number addition and storing # the results in appropriate variables(Different variables for real part and imaginary part of the sum)

# - The function should return both the above created variables enclosed as complex number object. For eg: If the variables to return are a & b, the return statement should be
# "return complex_numbers(a,b)"
# '-' (complex number subtraction)

# The difference between two complex numbers C= a+ ib and C'= a' + ib' is

# complex_diff

# - Create a function called __sub__ which takes two parameters 'self' and 'other'

# - Inside the function, should be statements performing complex number subtraction and storing the results in appropriate variables(Different variables for real part and imaginary part of the difference)

# - The function should return both the above created variables enclosed as complex number object
# '*' (complex number multiplication)

# The product of two complex numbers C=a+ ib and C'= a' + ib' is

# complex_prod

# The special case of a complex number C=a+ ib multiplied by a scalar a' is

# complex_prod2

# - Create a function called __mul__ which takes two parameters 'self' and 'other'

# - Inside the function, should be statements performing complex number multiplication and storing the results in appropriate variables(Different variables for real part and imaginary part of the product)

# - The function should return both the above created variables enclosed as complex number object
# '/' (complex number division)

# The division of two complex numbers C= a+ ib and C'= a' + ib' is done by multiplying the numerator and denominator by the complex conjugate of the denominator

# complex_quot

# Create a function called truediv which takes two parameters 'self' and 'other'.

# Inside the function, should be statements performing complex number division and storing the results in appropriate variables(Different variables for real part and imaginary part of the quotient)

# The function should return both the above created variables enclosed as complex number object

# Note: These operations should be compatible with int and float datatypes as well

    def __add__(self,other):
        result_real = self.real + other.real
        result_imag = self.imag + other.imag
        return complex_numbers(result_real,result_imag)

    def __sub__(self,other):
        result_real = self.real - other.real
        result_imag = self.imag - other.imag
        return complex_numbers(result_real,result_imag)

    def __mul__(self,other):
        result_real = (self.real * other.real) -  (self.imag * other.imag)
        result_imag = (self.imag * other.real) +  (self.real * other.imag)
        return complex_numbers(result_real,result_imag)

# Task 3:

# Define the following methods inside the class(These are not operation overload methods).

# absolute() function that returns absolute value of the complex number
# The absolute value of the complex number C= a+bi is the distance between the origin (0,0) # and the point (a,b) in the complex plane

# comp_abs

#   - Create a function called abs which takes one parameter 'self'.

#    - Inside the function, should be statements finding out the absolute value of the complex number and storing the result in an appropriate variable

#    - The function should return the above created variable
# argument() that returns argument of the complex number

# The argument of a complex number C=a+ ib is defined by this

# But for this task you can simply take it as,

# complex_arg

#   - Create a function called argument which takes one parameter 'self'.

#    - Inside the function, should be statements finding out the argument of the complex number and storing the result in an appropriate variable

#    - The function should return the above created variable
# conjugate() that returns conjugate of the complex number

# The conjugate of a complex number is the number with the same real part and an imaginary part equal in magnitude but opposite in sign

# complex_conj

# Create a function called conjugate which takes one parameter 'self'

# Inside the function, should be statements performing finding the conjugate of complex number and storing the results in appropriate variables(Different variables for real part and imaginary part of the quotient)

# The function should return both the above created variables enclosed as complex number object

    def __truediv__(self,other):
        result_real = ((self.real * other.real) +  (self.imag * other.imag))/(((other.real)**2) + ((other.imag)**2))
        result_imag = ((self.imag * other.real) -  (self.real * other.imag))/(((other.real)**2) + ((other.imag)**2))
        return complex_numbers(result_real,result_imag)

    def absolute(self):
        return (math.sqrt((self.real)**2 + (self.imag)**2))

    def argument(self):
        return math.degrees(math.atan((self.imag) / (self.real)))

    def conjugate(self):
        return complex_numbers(self.real,self.imag*(-1))

# Task 4

# After creating a class with all the above methods, let's create objects and implement its methods.

# Create an object with real part=3, imaginary part=5 and save it in a variable called 'comp_1'

comp_1 = complex_numbers(3,5)

print("comp_1:- " , comp_1)

# Create an object with real part=4, imaginary part=4 and save it in a variable called 'comp_2

comp_2 = complex_numbers(4,4)

print("comp_2:- " , comp_2)

# Implement addition of 'comp_1' and 'comp_2' and save the sum in a variable called 'comp_sum'

comp_sum = comp_1 + comp_2

print("comp_sum:- " , comp_sum)

# Implement subtraction of 'comp_1' and 'comp_2' and save the difference in a variable called 'comp_diff'

comp_diff = comp_1 - comp_2

print("comp_diff:- " , comp_diff)

# Implement multiplication of 'comp_1' and 'comp_2' and save the product in a variable called 'comp_prod'

comp_prod = comp_1 * comp_2

print("comp_prod:- " , comp_prod)

# Implement division of 'comp_1' and 'comp_2' and save the quotient in a variable called 'comp_quot'

comp_quot = comp_1 / comp_2

print("comp_quot:- " , comp_quot)

# Find the absolute value of comp_1 and save it in a variable called comp_abs

comp_abs = comp_1.absolute()

print("comp_abs:- " , comp_abs)

# Find the conjugate value of comp_1 and save it in a variable called comp_conj

comp_conj = comp_1.conjugate()

print("comp_conj:- " , comp_conj)

# Find the argument value of comp_1 and save it in a variable called comp_arg

comp_arg = comp_1.argument()

print("comp_arg:- " , comp_arg)


from solvers.utils.decomposition import cholesky_decomposition
from copy import deepcopy
import numpy as np

def cholesky_LU(A, coeff_b):
    """
    :param A: positive definite matrix otherwise it fails
    :param coeff_b: Array B in A * x = B
    :return: computed x values[ndarray]

    Uses same principle in LU decomposition
    For A * x = B
    1. It decomposes A into L, L' | A = L * L'
    2. From (1) => L * L' * x = B. Notation: L' * x = y
    3. From (1), (2) =>
                        L * y = B (I)
                        L' * x = y (II)
    4. First forward solve (I), than back solve (II)
    """
    try:
        L = cholesky_decomposition(A)

        assert np.dot(L, L.T) == A

        y = __forward_substitution(L, coeff_b)
        x = __back_substitution(L.T, y)

        return x

    except Exception as e:
        raise e

def __forward_substitution(lower_t, coeff, d_type=np.float64):
    """
    :param lower_t: Lower triangular matrix
    :param coeff: Array B in L * y = B
    :param d_type: Elements data type
    :return: computed y values for back substitution
    """
    n = len(lower_t)
    lt = deepcopy(lower_t)
    res = np.zeros(shape=(n, 1))
    for i in range(n):
        terms_sum = sum(lt[i][j] * lt[j][j] for j in range(i))
        res[i] = d_type((coeff[i][0] - terms_sum) / lt[i][i])
        lt[i][i] = res[i]
    return res

def __back_substitution(upper_t, coeff, d_type=np.float64):
    """
    :param upper_t: Upper triangular matrix
    :param coeff: Array y in L' * x = y
    :param d_type: Elements data type
    :return: computed x values, solution
    """
    n = len(upper_t)
    ut = deepcopy(upper_t)
    res = np.zeros(shape=(n, 1))
    for i in range(n - 1, -1, -1):
        terms_sum = sum(ut[i][j] * ut[j][j] for j in range(n - 1, i, -1))
        res[i] = d_type((coeff[i][0] - terms_sum) / ut[i][i])
        ut[i][i] = res[i]
    return res


from __future__ import annotations
import numpy as np

class SimpleLinearRegression:
    """Simple Linear Regression
    Parameters:
    -----------
    learning_rate: float
        The step length used when following the negative gradient during training.
    """
    def __init__(self, learning_rate: float) -> None:
        self.m = 0
        self.b = 0
        self.learning_rate = learning_rate

    def cost_function(self, x: np.ndarray, y: np.ndarray) -> float:
        total_error = 0
        for i in range(0, len(x)):
            total_error += (y[i]-(self.m*x[i]+self.b))**2
        return total_error/float(len(x))

    def fit(self, x: np.ndarray, y: np.ndarray, num_iterations: int) -> SimpleLinearRegression:
        N = float(len(x))
        for j in range(num_iterations):
            b_gradient = 0
            m_gradient = 0
            for i in range(0, len(x)):
                b_gradient += -(2/N) * (y[i] - ((self.m * x[i]) + self.b))
                m_gradient += -(2/N) * x[i] * \
                    (y[i] - ((self.m * x[i]) + self.b))
            self.b -= (self.learning_rate * b_gradient)
            self.m -= (self.learning_rate * m_gradient)
        return self

    def predict(self, xs: np.ndarray) -> list:
        return [(self.m * x + self.b) for x in xs]

# Testing functionality
if __name__ == '__main__':
    x = np.linspace(0, 100, 50)
    delta = np.random.uniform(-10, 10, x.size)
    y = 0.5 * x + 3 + delta

    model = SimpleLinearRegression(0.0001)
    model.fit(x, y, 100)
    print('Error:', model.cost_function(x, y))


import numpy as np

#GE and BS

#GE
A=np.array([[1,-1,0,0]
           ,[0,1,-1,0]
           ,[0,0,1,-1]
           ,[-1,0,0,1]])

B=np.array([[160]
           ,[-40]
           ,[210]
           ,[-330]])

#k=0,j=1,pass
#k=0,j=2,pass
#k=0,j=3
lam=A[3,0]/A[0,0]
A[3,:]=A[3,:]-lam*A[0,:]
B[3]=B[3]-lam*B[0]

print(np.c_[A,B])
# [[   1   -1    0    0  160]
#  [   0    1   -1    0  -40]
#  [   0    0    1   -1  210]
#  [   0   -1    0    1 -170]]

#k=1,j=3
lam=A[3,1]/A[1,1]
A[3,:]=A[3,:]-lam*A[1,:]
B[3]=B[3]-lam*B[1]

print(np.c_[A,B])
# [[   1   -1    0    0  160]
#  [   0    1   -1    0  -40]
#  [   0    0    1   -1  210]
#  [   0    0   -1    1 -210]]

#k=2,j=3
lam=A[3,2]/A[2,2]
A[3,:]=A[3,:]-lam*A[2,:]
B[3]=B[3]-lam*B[2]

print(np.c_[A,B])
# [[  1  -1   0   0  160]
#  [  0   1  -1   0  -40]
#  [  0   0   1  -1  210]
#  [  0   0   0   0    0]]

def GE(A,B):
    for k in range(0,len(A)):
        for j in range(k+1,len(A)):
            if A[j,k]!=0:
                lam=A[j,k]/A[k,k]
                A[j,:]=A[j,:]-lam*A[k,:]
                B[j]=B[j]-lam*B[k]
                print(np.c_[A,B])
    return A,B,np.c_[A,B]

A=np.array([[1,-1,0,0]
           ,[0,1,-1,0]
           ,[0,0,1,-1]
           ,[-1,0,0,1]])

B=np.array([[160]
           ,[-40]
           ,[210]
           ,[-330]])

A,B,total=GE(A,B)
# [[  1  -1   0   0 160]
#  [  0   1  -1   0 -40]
#  [  0   0   1  -1 210]
#  [  0   0   0   0   0]]

#BS
sol=np.zeros((len(A)))
#x_4=100
sol[3]=100
sol[2]=(B[2]-A[2,3]*sol[3])/A[2,2]
sol[1]=(B[1]-A[1,2]*sol[2])/A[1,1]
sol[0]=(B[0]-A[0,1]*sol[1])/A[0,0]
print("x_0=",sol[0])
print("x_1=",sol[1])
print("x_2=",sol[2])
print("x_3=",sol[3])
# x_0= 430.0
# x_1= 270.0
# x_2= 310.0
# x_3= 100.0


import numpy as np

def saturate_upper_to_one(x, b):
    t = x - b
    k = 1 / (1 - b)
    return b / (b + (1 - b) * np.exp(-t * k))

def saturate_lower_to_zero(x, a):
    t = a - x
    k = 1 / a
    return a / np.exp(t * k)

def percentile_scale_clean(x, a=.05, b=.95):
    # compute upper and lower pctl values
    va, vb = np.percentile(x, q=(a * 100, b * 100))

    # scale x=[va,vu] to y=[a,b]
    to1 = (x - va) / (vb - va)
    y = a + b * to1

    # saturate upper values to [b,1]
    idx = y > b
    y[idx] = saturate_upper_to_one(y[idx], b)

    # fit lower values to [0,a]
    idx = y < a
    y[idx] = saturate_lower_to_zero(y[idx], a)

    # done
    return y, va, vb

def check_ignore(e, naignore=[]):
    """True if the element is nan, inf, None, or
    from a specified list with excluded values
    """
    if e:   # Check None
        if np.isnan(e):
            return True
        if np.isinf(e):
            return True
        if naignore:
            if e in naignore:
                return True
    else:
        return True
    return False

def percentile_scale(x, a=.05, b=.95, naignore=[0], naimpute=0):
    # ensure numpy array
    x_ = np.array(x)
    # memorize ineligible values
    idxmiss = np.array([check_ignore(e, naignore) for e in x_])
    idxexist = np.logical_not(idxmiss)

    y, va, vb = percentile_scale_clean(x_[idxexist], a=a, b=b)

    z = np.empty(shape=x_.shape)
    z[idxexist] = y
    z[idxmiss] = naimpute

    return z, va, vb

def inverse_ztoy_upper(z, b):
    oneminusb = 1 - b
    return b - oneminusb * np.log(((b / z) - b) / oneminusb)

def inverse_ztoy_lower(z, a):
    return a * (np.log(z / a) + 1)

def inverse(z, a, b, va, vb):
    # the linear case z=y (nothing to inverse. just copy)
    y = z.copy()

    # inverse z below threshold a
    idx = z < a
    y[idx] = inverse_ztoy_lower(z[idx], a)

    # inverse z above threshold b
    idx = z > b
    y[idx] = inverse_ztoy_upper(z[idx], b)

    # inverse the min-max scaling y to x
    return ((y - a) / b) * (vb - va) + va


# -*- coding: utf-8 -*-
"""
Created on Wed Jan 15 15:50:13 2020
"""

import random
import itertools
import networkx as nx
import operator as op
from functools import reduce

def get_random_graph(n, p, k=None):
    """Random graph of n nodes and prob p for each edge
    k: int/None. Adds constraint that note i not adjacent to j if i-j = 0 mod k
    """
    nodes = set(range(1, n+1))
    edges = set(itertools.combinations(nodes, 2))
    G = nx.Graph()
    G.add_nodes_from(nodes)
    for edge in edges:
        if random.random() < p:
            G.add_edges_from([edge])

    if not k is None:
        for i, j in [[i, j]  for i, j in G.edges if (i-j) % k == 0]:
            G.remove_edge(i, j)

    return G

####
# Different graph orderings
####

def incr_deg_ordering(G):
    """returns list of nodes ordered by increasing degree"""
    node_degrees = [[node, G.degree(node)] for node in G.nodes]
    node_degrees.sort(key=lambda x: x[1])
    return [x[0] for x in node_degrees]

def decr_deg_ordering(G):
    """returns list of nodes ordered by decreasing degree"""
    node_degrees = [[node, G.degree(node)] for node in G.nodes]
    node_degrees.sort(key=lambda x: x[1], reverse=True)
    return [x[0] for x in node_degrees]

def min_sub_deg_ordering(G):
    """returns ordering of nodes {v1,..., vn} where
    starting from vn, vn := min deg v in G
    vn-1 := min deg v in G - {vn},
    vj := min deg v in G - {vj+1, ..., vn}"""

    node_degrees = [[node, G.degree(node)] for node in G.nodes]
    ordering = []

    for _ in range(len(node_degrees)):
        node_degrees.sort(key=lambda x: x[1])   # sorted from min to max degree
        node = node_degrees.pop(0)[0]           # take min degree node as vj
        ordering.append(node)
        for node_degree in node_degrees:        # update degrees per removing vj
            if G.has_edge(node, node_degree[0]):
                node_degree[1] -= 1

    #reverse the ordering to get [v1, v2, ..., vn]
    return list(reversed(ordering))

# min_sub_deg_ordering and max_sub_deg_ordering sound similar but not same, e.g.
# [1,2,3,4,5,6,7] nodes and [[1,4], [2,4], [3,4], [5,6], [6,7], [7,5]] edges

def max_sub_deg_ordering(G):
    """returns ordering of nodes {v1,..., vn} where
    starting from v1, take v1 := max deg v in G
    v2 := max deg v in G - {v1},
    vj := min deg v in G - {v1, ..., vj-1}"""

    node_degrees = [[node, G.degree(node)] for node in G.nodes]
    ordering = []

    for _ in range(len(node_degrees)):
        # sorted from max to min degree
        node_degrees.sort(key=lambda x: x[1], reverse=True)
        # take max degree node as vj
        node = node_degrees.pop(0)[0]
        ordering.append(node)
        for node_degree in node_degrees:       # update degrees per removing vj
            if G.has_edge(node, node_degree[0]):
                node_degree[1] -= 1

    return ordering

def random_ordering(G):
    nodes = list(G.nodes)
    random.shuffle(nodes)
    return nodes

def get_complete_graph(n):
    """Returns a K_n"""
    G = nx.Graph()
    nodes = list(range(1, n+1))
    G.add_nodes_from(nodes)
    G.add_edges_from(list(itertools.combinations(nodes, 2)))
    return G

def factorial(n):
    """factorial(4) = 4*3*2*1 = 4! = 24"""
    return reduce(op.mul, range(1, n+1))

def ncr(n, r):
    """n choose r e.g. ncr(n, 2) = n*(n-1)/2"""
    r = min(r, n-r)
    numer = reduce(op.mul, range(n, n-r, -1), 1)
    denom = reduce(op.mul, range(1, r+1), 1)
    return numer / denom

def expected_num_ks(n, p, s):
    """expected number of K_s complete subgraphs in randomly generated G(n, p)
    """
    return ncr(n, s) * p ** ncr(s, 2)

def variance_num_ks(n, p, s):
    """variance of number of K_s complete subgraphs in randomly generated G(n, p)
    """
    output = 0
    for l in range(2, s+1):
        output += ncr(n-s, s-l) * ncr(s, l) * (p**(-ncr(l, 2)) - 1)

    output *= ncr(n, s) * p ** ncr(s, 2)
    return output

def rough_expected_node_num_cliques(n, p, j):
    """This function doesn't seem quite right as I thought it would agree with
    expected_num_ks, i.e. f(n, p, j) = g(pn, p, j-1) for f, g = rough,expect
    This func aims to estimate the expected number of K_j a randomly chosen node
    of G(n, p) would be in"""
    j = j-1
    return n**j * p**(j*(j+1)/2) / factorial(j)


import numpy as np

# 6x6 행렬 만들기
a = np.array([i for i in range(36)]).reshape(6,6)

# 3x3 필터 만들기
Filter = np.eye(3,3)

# 행렬 확인
print('---a\n',a)
print('---filter\n',Filter)

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

### 합성곱 연산 방법 1 ###

# 단일 곱셈-누산 vs 행렬곱 연산
d = np.array([[1,2,3],[4,5,6],[7,8,9]])
print('---d\n',d)
print('---단일 곱셈-누산 결과\n', np.sum(Filter * d)) # (1 * 1) + (-1 * 2) + (-1 * 3) + (1 * 4)
print('---행렬곱 연산 결과\n', np.dot(Filter, d))

# 넘파이 array indexing
print('---a[:,:]\n',a[:,:])            # a 전체 출력
print('---a[:,1:2]\n',a[:,0:3])      # a의 전체행 / 첫번째열~세번째열 출력
print('---a[0:3,4:5]\n',a[3:5,4:5])    # a의 네번재행~다섯번째행 / 다섯번째열 출력

# 스트라이드
for rn in range(len(a[0])-1):
    for cn in range(len(a[1])-1):
        print('---',[rn,cn],'\n',a[rn:rn+2, cn:cn+2])

# 합성곱 연산
result = []

for rn in range(len(a[0])-2):
    for cn in range(len(a[1])-2):
        result.append(np.sum(a[rn:rn+3, cn:cn+3] * Filter))

print('---result\n',result)
print('---len(result)\n', len(result))
len_a = int(np.sqrt(len(result)))
result = np.array(result).reshape(len_a,len_a)
print('---result.reshape\n', result)

# 패딩
a_pad = np.pad(a, pad_width=1, mode='constant', constant_values=0)
print('---a_pad\n',a_pad)

a_pad2 = np.pad(a, pad_width=2, mode='constant', constant_values=-1) # constant_values로 숫자 변경 가능
print('---a_pad2\n',a_pad2)

a_pad3 = np.pad(a, pad_width=((1,2),(3,4)), mode='constant', constant_values=0) # pad_width=( (위, 아래), (왼쪽, 오른쪽 패드 수) )
print('---a_pad3\n',a_pad3)

# 패딩 적용한 합성곱 연산
result2 = []

for rn in range(len(a_pad[0])-2):
    for cn in range(len(a_pad[1])-2):
        result2.append(np.sum(a_pad[rn:rn+3, cn:cn+3] * Filter))

print('---result2\n', result2)
print('---len(result2)\n', len(result2))
len_a2 = int(np.sqrt(len(result2)))
result2 = np.array(result2).reshape(len_a2, len_a2)
print('---result2.reshape\n', result2)

# 문제(1). 0부터 143까지 원소로 이뤄진 12x12 행렬을 만들고, 4x4 필터(단위 행렬)를 이용해 합성곱을 해보세요.
#         (단, 스트라이드는 1, 출력 행렬은 12x12가 되도록 패딩을 적용하세요)
x = np.arange(0, 144).reshape((-1, 12))
x = np.pad(x, pad_width=2, mode='constant', constant_values=0)
f = np.eye(4, 4)

result = []

for rn in range(len(x[0])-4):
    for cn in range(len(x[1])-4):
        result.append(np.sum(x[rn:rn+4, cn:cn+4] * f))
result = np.array(result).reshape(-1, 12)
print(result)
print(result.shape)

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

### 합성곱 연산 2 ###

# 단일 곱셈 누산 -> 행렬곱 연산
print('---Filter\n', Filter)
print('---Filter.flatten()\n', Filter.flatten())  # flatten은 행렬을 벡터로 만들어줌

# 행렬곱하기 좋게 행렬을 변환해주는 함수
def im2col_sliding_strided(A, filtersize, stepsize=1): # A = 변환할 행렬, filtersize = 필터 크기, stepsize = 스트라이드
    m, n = A.shape
    s0, s1 = A.strides
    BSZ = [m + 1 - filtersize[0], n + 1 - filtersize[1]]
    nrows = m - BSZ[0] + 1
    ncols = n - BSZ[1] + 1
    shp = BSZ[0], BSZ[1], nrows, ncols
    strd = s0, s1, s0, s1

    out_view = np.lib.stride_tricks.as_strided(A, shape=shp, strides=strd)
    return out_view.reshape(BSZ[0] * BSZ[1], -1)[:, ::stepsize]

print('---변환 전 a\n', a)
print('---변환 후 a\n', im2col_sliding_strided(a, [3,3]))

# 행렬곱 연산을 이용한 합성곱

a_pad = np.pad(a, pad_width=1, mode='constant', constant_values=0)
a2 = im2col_sliding_strided(a_pad, [3,3])
Filter2 = Filter.flatten()
result = np.dot(a2, Filter2)
print('---합성곱 결과\n', result)
result = result.reshape(6,6)
print('---최종 결과\n', result)

# 문제(2). 앞에서 배운 두 가지 합성곱 방법을 각각 이용하여 0~1사이의 난수로 이루어진 300x300 행렬을
#          9x9 필터(단위행렬)를 이용해 합성곱을 해보세요. (단, 스트라이드는 1, 출력 행렬 크기는 300x300이 되도록 패딩을 적용하세요)

print('====================================================================================================')
print('== 문제 154. 0부터 15까지 원소로 이루어진 4x4 행렬을 만드시오!')
print('====================================================================================================\n')
a = np.arange(0, 16).reshape((4, 4))

print('====================================================================================================')
print('== 문제 155. 위에서 만든 행렬에 0 패딩 1을 수행하시오.')
print('====================================================================================================\n')
a = np.pad(a, pad_width=1, mode='constant', constant_values=0)

print('====================================================================================================')
print('== 문제 156. 0부터 35까지 원소로 이루어진 6x6 행렬을 만들고 0부터 15까지의 원소로 이루어진 4x4 필터를'
      '이용해서 합성곱을 하시오!')
print('====================================================================================================\n')
x = np.arange(0, 36).reshape((-1, 6))
x = np.pad(x, pad_width=2, mode='constant', constant_values=0)
f = np.arange(0, 16).reshape((4, 4))

result = []

for rn in range(x.shape[0]-4):
    for cn in range(x.shape[1]-4):
        result.append(np.sum(x[rn:rn+4, cn:cn+4] * f))
result = np.array(result).reshape(-1, 6)
print(result)
print(result.shape)

print('====================================================================================================')
print('== 문제 158. 아래와 같이 출력값(OH)와 Stride(S) 와 입력값(H)와 필터값(FH)를 입력하면 P(패딩)이 출력이 되는 함수를 생성하시오.')
print('====================================================================================================\n')
import math
padding = lambda OH,S,H,FH : math.ceil(((OH-1)*S-H+FH)/2)

print('====================================================================================================')
print('== 문제 159. 0부터 15까지 원소로 이루어진 4x4 행렬을 만들고 0부터 8까지의 원소로 이루어진 3x3 필터를'
      '이용해서 합성곱을 하시오!')
print('====================================================================================================\n')
x = np.arange(0, 16).reshape((-1, 4))
x = np.pad(x, pad_width=1, mode='constant', constant_values=0)
f = np.arange(0, 9).reshape((-1, 3))

result = []
x
for rn in range(x.shape[0]-2):
    for cn in range(x.shape[1]-2):
        result.append(np.sum(x[rn:rn+3, cn:cn+3] * f))
result = np.array(result).reshape(-1, 4)
print(result)
print(result.shape)

print('====================================================================================================')
print('== 문제 160. 0부터 35까지 원소로 이루어진 6x6 행렬을 만들고 0부터 8까지의 원소로 이루어진 3x3 필터를'
      '이용해서 합성곱을 하시오!')
print('====================================================================================================\n')
x = np.arange(0, 36).reshape((-1, 6))
x = np.pad(x, pad_width=1, mode='constant', constant_values=0)
f = np.arange(0, 9).reshape((-1, 3))

result = []
x
for rn in range(x.shape[0]-2):
    for cn in range(x.shape[1]-2):
        result.append(np.sum(x[rn:rn+3, cn:cn+3] * f))
result = np.array(result).reshape(-1, 6)
print(result)
print(result.shape)

print('====================================================================================================')
print('== 문제 160. 0부터 35까지 원소로 이루어진 6x6 행렬을 만들고 0부터 8까지의 원소로 이루어진 3x3 필터를'
      '이용해서 합성곱을 하시오!')
print('====================================================================================================\n')
x = np.arange(0, 25).reshape((-1, 5))
x = np.pad(x, pad_width=1, mode='constant', constant_values=0)
f = np.arange(0, 4).reshape((-1, 2))

result = []

for rn in range(x.shape[0]-2):
    for cn in range(x.shape[1]-2):
        result.append(np.sum(x[rn:rn+2, cn:cn+2] * f))
result = np.array(result).reshape(-1, 5)
print(result)
print(result.shape)

print('====================================================================================================')
print('== 문제 163. 입력 데이터를 15행의 8열로 0~119번까지의 원소로 생성해서 채널을 10개로 만든걸 입력 데이터를 만들고'
      '필터를 3행에 3열로 해서 0~8 까지의 원소로 생성해서 채널을 10개로 생성해서 합성곱을 구현하시오!')
print('====================================================================================================\n')
x = np.array([np.arange(0, 120).reshape((15, 8)) for _ in range(10)])
f = np.array([np.arange(0, 9).reshape((3, 3)) for _ in range(10)])

for idx in range(x.shape[0]):
    temp = np.pad(x[idx], pad_width=1, mode='constant', constant_values=0)
    result = []
    for rn in range(temp.shape[0]-2):
        for cn in range(temp.shape[1]-2):
            result.append(np.sum(temp[rn:rn+3, cn:cn+3] * f))
    result = np.array(result).reshape(-1, 8)
    print(result)
    print(result.shape)

print('====================================================================================================')
print('== NCS 문제 3. 0부터 80까지의 원소로 이루어진 9x9 행렬을 만들고 0부터 15까지의 원소로 이루어진 4x4 필터를 이용해서 합성곱을 하시오!')
print('====================================================================================================\n')
import numpy as np
x = np.arange(0, 81).reshape((-1, 9))
x = np.pad(x, pad_width=2, mode='constant', constant_values=0)
f = np.arange(0, 16).reshape((-1, 4))

result = []

for rn in range(x.shape[0]-4):
    for cn in range(x.shape[1]-4):
        result.append(np.sum(x[rn:rn+4, cn:cn+4] * f))
result = np.array(result).reshape(-1, 9)
print(result)
print(result.shape)

print('====================================================================================================')
print('== NCS 문제 4. 문제 3번을 다시 수행하는데 원소의 값은 똑같고 입력값의 채널이 3차원일때의 합성곱을 구하시오!')
print('====================================================================================================\n')
x = np.array([np.arange(0, 81).reshape((-1, 9)) for _ in range(10)])
f = np.array([np.arange(0, 16).reshape((-1, 4)) for _ in range(10)])

for idx in range(x.shape[0]):
    temp = np.pad(x[idx], pad_width=2, mode='constant', constant_values=0)
    result = []
    for rn in range(temp.shape[0]-4):
        for cn in range(temp.shape[1]-4):
            result.append(np.sum(temp[rn:rn+4, cn:cn+4] * f))
    result = np.array(result).reshape(-1, 9)
    print(result)
    print(result.shape)

print('====================================================================================================')
print('== 문제 164. (점심시간 문제) 문제 162번 최대 풀링을 파이썬으로 구현하시오.')
print('====================================================================================================\n')
a = np.array([[21, 8, 8, 12], [12, 19, 9, 7], [8, 10, 4, 3], [18, 12, 9, 10]])

def pooling(x):
    result = []
    w, h = x.shape
    for i in range(0, w, 2):
        for j in range(0, h, 2):
            result.append(np.max(x[i:i+2, j:j+2]))
    return np.array(result).reshape((2, 2))
print(pooling(a))

print('====================================================================================================')
print('== 문제 167. 0부터 15까지 원소로 이루어진 4x4 행렬을 만들고 0부터 8까지의 원소로 이루어진 3x3 필터를 이용해서'
      '합성곱을 하는데 이번에는 im2col 을 활용해서 수행하시오.')
print('====================================================================================================\n')
import numpy as np
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
    """다수의 이미지를 입력받아 2차원 배열로 변환한다(평탄화).

    Parameters
    ----------
    input_data : 4차원 배열 형태의 입력 데이터(이미지 수, 채널 수, 높이, 너비)
    filter_h : 필터의 높이
    filter_w : 필터의 너비
    stride : 스트라이드
    pad : 패딩

    Returns
    -------
    col : 2차원 배열
    """
    C, H, W = input_data.shape
    N = 1
    out_h = (H + 2 * pad - filter_h) // stride + 1
    out_w = (W + 2 * pad - filter_w) // stride + 1

    img = np.pad(input_data, [(0, 0), (0, 0), (pad, pad), (pad, pad)], 'constant')
    col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))

    for y in range(filter_h):
        y_max = y + stride * out_h
        for x in range(filter_w):
            x_max = x + stride * out_w
            col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]

    col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N * out_h * out_w, -1)
    return col

x = np.array([np.arange(0, 16).reshape((-1, 4)) for _ in range(10)])
x = np.pad(x, pad_width=1, mode='constant', constant_values=0)
f = np.array([np.arange(0, 9).reshape((-1, 3)) for _ in range(10)])

result = im2col(x, 3, 3, 1)
print(result)
print(result.shape)


"""
    Copyright (c) 2019, Etienne de Foras and the respective contributors
    All rights reserved.

    Use of this source code is governed by a MIT license that can be found
    in the LICENSE.txt file.
"""

import numpy as np
import copy
import Optimizer as opt

def compute_confusion_matrix(truth,predicted,nb_class=0):
	if nb_class==0:
		nb_class=np.max(truth)
	confmat=np.zeros((nb_class,nb_class),dtype=int)
	np.add.at(confmat, tuple([truth.ravel(),predicted.ravel()]), 1)
	accuracy=100.*np.trace(confmat)/np.sum(confmat)
	return confmat,accuracy

def to_one_hot(label,nb_class):
  train_label_one_hot=np.eye(nb_class)[label]
  train_label_one_hot = train_label_one_hot.reshape(label.shape[0], nb_class)
  return train_label_one_hot

################################################################################################### Layers
class Layer:
  def __init__(self):
    self.dydx = 1.
    self.learnable = False # set to False to freeze layer or if not learnable
    self.training = False # set to False in testing mode or True in training mode
    self.learnBias= False

  def init(self):
    pass

  def forward(self,x):
    pass

  #compute input loss from output loss
  def backpropagation(self,dldy):
    return dldy * self.dydx

# see https://stats.stackexchange.com/questions/115258/comprehensive-list-of-activation-functions-in-neural-networks-with-pros-cons
class LayerAbsolute(Layer):
  def forward(self,x):
    if self.training:
      self.dydx = np.sign(x)
    return np.abs(x)

# see http://mathworld.wolfram.com/InverseHyperbolicSine.html
class LayerArcSinh(Layer):
  def forward(self,x):
    if self.training:
      self.dydx = 1. / np.sqrt(1. + x * x)
    return np.arcsinh(x)

class LayerArcTan(Layer):
  def forward(self,x):
    if self.training:
      self.dydx = 1. / (1. + x * x)
    return np.arctan(x)

class LayerBent(Layer):
  def forward(self,x):
    if self.training:
      self.dydx = x/(2.*np.sqrt(x*x+1.))+1.
    return (np.sqrt(x*x+1.)-1.)*0.5+x

#see Binary Step as in: https://en.wikipedia.org/wiki/Activation_function
class LayerBinaryStep(Layer):
  def forward(self,x):
    if self.training:
      self.dydx = x * 0.
    return (x > 0.) * 1.

# see https://stats.stackexchange.com/questions/115258/comprehensive-list-of-activation-functions-in-neural-networks-with-pros-cons
class LayerBipolar(Layer):
  def forward(self,x):
    if self.training:
      self.dydx = x*0.
    return np.sign(x)

#see https://adl1995.github.io/an-overview-of-activation-functions-used-in-neural-networks.html
class LayerBipolarSigmoid(Layer):
  def forward(self,x):
    s=np.exp(x)
    if self.training:
      self.dydx=2.*s/((s+1.)*(s+1.))
    return (s-1.)/(s+1.)

# see https://stats.stackexchange.com/questions/115258/comprehensive-list-of-activation-functions-in-neural-networks-with-pros-cons
class LayerComplementaryLogLog(Layer):
  def forward(self,x):
    if self.training:
      self.dydx = np.exp(x - np.exp(x))
    return 1. - np.exp(-np.exp(x))

class LayerDivideBy256(Layer): # usefull for fixpt code conversion
  def forward(self,x):
    if self.training:
      self.dydx = 0.00390625 # 0.00390625 == 1./256.
    return x*0.00390625

# see Sigmoid-Weighted Linear Units for Neural Network Function ; Stefan Elfwinga Eiji Uchibea Kenji Doyab
class LayerdSiLU(Layer):
  def forward(self,x):
    ex=np.exp(-x)
    exinv=1./(1.+ex)
    if self.training:
      self.dydx = ex*exinv*exinv*(2.+x*(2.*ex*exinv-1.))
    return exinv*(1+x*ex*exinv)

class LayerExponential(Layer):
  def forward(self,x):
    e=np.exp(x)
    if self.training:
      self.dydx = e
    return e

class LayerGauss(Layer):
  def forward(self,x):
    u=np.exp(-x*x)
    if self.training:
      self.dydx = -2.*x*u
    return u

# see https://cs224d.stanford.edu/lecture_notes/LectureNotes3.pdf
class LayerHardTanh(Layer):
  def forward(self,x):
    if self.training:
      self.dydx=np.ones(x.shape,dtype=np.float32)
      self.dydx[x<-1.]=0.
      self.dydx[x>1.]=0.
    u=x
    u[x<-1.] = -1.
    u[x>1.] = 1.
    return u

# HardELU, ELU fixed point approximation ; Author is Minh Tri LE
class LayerHardELU(Layer):
  def forward(self,x):
    if self.training:
      self.dydx=np.ones(x.shape,dtype=np.float32)
      self.dydx[ (x<-0.) & (x>-2.) ]=0.5
      self.dydx[ x<-2. ]=0.
    u=x
    u[ (x<-0.) & (x>-2.) ] *= 0.5
    u[x<-2.] = -1.
    return u

# see  https://nn.readthedocs.io/en/rtd/transfer/ (lambda=0.5)
class LayerHardShrink(Layer):
  def forward(self,x):
    if self.training:
      self.dydx=np.ones(x.shape,dtype=np.float32)
      self.dydx[(x<0.5) & (x>-0.5)]=0.
    u=x
    u[ (x<0.5) & (x>-0.5) ] = 0.
    return u

class LayerIdentity(Layer):
  def forward(self,x):
    return x

class LayerLeakyRELU(Layer):
  def forward(self,x):
    if self.training:
      self.dydx=np.ones(x.shape,dtype=np.float32)
      self.dydx[x<0.]=0.01
    u=x
    u[u<0.] *= 0.01
    return u

# easy to convert in fixedpoint (0.00390625= 1/256 = (1>>8) )
class LayerLeakyRELU256(Layer):
  def forward(self,x):
    if self.training:
      self.dydx=np.ones(x.shape,dtype=np.float32)
      self.dydx[x<0.]=0.00390625
    u=x
    u[u<0.] *= 0.00390625
    return u

#see LogSigmoid as in : https://nn.readthedocs.io/en/rtd/transfer/
class LayerLogSigmoid(Layer):
  def forward(self,x):
    if self.training:
      self.dydx = 1./(1.+np.exp(x))
    return np.log(1./(1+np.exp(-x)))

#see Logit as in : https://adl1995.github.io/an-overview-of-activation-functions-used-in-neural-networks.html
class LayerLogit(Layer):
  def forward(self,x):
    if self.training:
      self.dydx = -x/(x-1.)
    return np.log(x/(1.-x))

class LayerRELU(Layer):
  def forward(self,x):
    if self.training:
      self.dydx = (x > 0.) * 1.
    return (x > 0.) * x

class LayerRELU6(Layer):
  def forward(self,x):
    if self.training:
      self.dydx=np.ones(x.shape,dtype=np.float32)
      self.dydx[x<0.]=0.
      self.dydx[x>6.]=0.
    u=x
    u[x<0.] = 0.
    u[x>6.] = 6.
    return u

class LayerSigmoid(Layer):
  def forward(self,x):
    y = 1. / (1. + np.exp(-x))
    if self.training:
      self.dydx = y * (1. - y)
    return y

class LayerSoftPlus(Layer):
  def forward(self,x):
    if self.training:
      self.dydx = 1. / (1. + np.exp(-x))
    return np.log1p(np.exp(x))

class LayerSoftSign(Layer):
  def forward(self,x):
    d=1.+np.abs(x)
    if self.training:
      self.dydx = 1. / (d*d)
    return x/d

# see paper: Swish: A Self-Gated Activation Function
class LayerSwish(Layer):
  def forward(self,x):
    y = 1. / (1. + np.exp(-x))
    if self.training:
      self.dydx = y * (x + 1. - x * y)
    return x * y

# see Sigmoid-Weighted Linear Units for Neural Network Function ; Stefan Elfwinga Eiji Uchibea Kenji Doyab
class LayerSiLU(Layer):
  def forward(self,x):
    ex=np.exp(-x)
    exinv=1./(1.+ex)
    if self.training:
      self.dydx = exinv*(1+x*ex*exinv)
    return x *exinv

# see https://en.wikipedia.org/wiki/Activation_function
class LayerSin(Layer):
  def forward(self,x):
    if self.training:
      self.dydx = np.cos(x)
    return np.sin(x)

class LayerTanh(Layer):
  def forward(self,x):
    y = np.tanh(x)
    if self.training:
      self.dydx = 1. - y * y
    return y

class LayerTanhShrink(Layer):
  def forward(self,x):
    y = np.tanh(x)
    if self.training:
      self.dydx = y * y
    return x-y

####################################### special layers
class LayerGlobalBias(Layer):
  def __init__(self):
    super().__init__()
    self.learnBias= True
    self.b = np.zeros(1,dtype=np.float32)

  def forward(self,x):
    return x + self.b[0]

  def backpropagation(self,dldy):
    self.dldb = np.atleast_1d(dldy.mean())
    return dldy

class LayerBias(Layer):
  def __init__(self,outSize):
    super().__init__()
    self.learnBias= True
    self.b = np.zeros((1,outSize),dtype=np.float32)

  def forward(self,x):
    return x + self.b

  def backpropagation(self,dldy):
    self.dldb = np.atleast_2d(np.mean(dldy,axis=0))
    return dldy

class LayerGlobalGain(Layer):
  def __init__(self):
    super().__init__()
    self.learnable = True
    self.w = np.ones(1,dtype=np.float32)

  def forward(self,x):
    if self.training:
      self.dydx = self.w[0]
      self.dydw = x
    return x * self.w[0]

  def backpropagation(self,dldy):
    self.dldw = np.atleast_1d((self.dydw * dldy).mean())
    return dldy * self.dydx

class LayerAddGaussianNoise(Layer):
  def __init__(self,stdev=0.1):
    super().__init__()
    self.stdev=stdev

  def forward(self,x):
    if self.training:
      x += np.random.normal(0, self.stdev,x.shape)
    return x

class LayerAddUniformNoise(Layer):
  def __init__(self,noise=0.1):
    super().__init__()
    self.noise=noise

  def forward(self,x):
    if self.training:
      x += np.random.rand(*(x.shape))*self.noise
    return x

class LayerDenseNoBias(Layer):
  def __init__(self,inSize,outSize):
    super().__init__()
    self.learnable = True
    self.inSize=inSize
    self.outSize=outSize
    self.init()

  def init(self):
    a=np.sqrt(6./(self.inSize+self.outSize)) # Xavier uniform initialization
    self.w = a*(np.random.rand(self.inSize,self.outSize) * 2. - 1.)

  def forward(self,x):
    if self.training:
      self.dydw = x
    return x @ self.w

  def backpropagation(self,dldy):
    self.dldw = self.dydw.transpose() @ dldy
    self.dldw *= (1./(self.dydw.shape[0]))
    return dldy @ (self.w.transpose())

class LayerDense(Layer): # with bias
  def __init__(self,inSize,outSize):
    super().__init__()
    self.inSize=inSize
    self.outSize=outSize
    self.learnable = True
    self.learnBias = True
    self.init()

  def init(self):
    a=np.sqrt(6./(self.inSize+self.outSize)) # Xavier uniform initialization
    self.w = a*(np.random.rand(self.inSize,self.outSize) * 2. - 1.)
    self.b = np.zeros((1,self.outSize),dtype=np.float32)

  def forward(self,x):
    if self.training:
      self.dydw = x
    return (x @ self.w) + self.b

  def backpropagation(self,dldy):
    self.dldw = self.dydw.transpose() @ dldy
    self.dldw *= (1./(self.dydw.shape[0]))
    self.dldb = np.atleast_2d(np.mean(dldy,axis=0))
    return dldy @ (self.w.transpose())

class LayerSoftmax(Layer):
  def __init__(self):
    super().__init__()

  def forward(self,x):
    max_row=np.max(x,axis=1)
    ex=np.exp(x-max_row[:,None])
    sum_row=np.atleast_2d(np.sum(ex,axis=1)).transpose()
    if self.training:
      self.dydx=-ex*(ex-sum_row)/(sum_row*sum_row)
    return ex/sum_row

class LayerDropout(Layer):
  def __init__(self,rate=0.3):
    super().__init__()
    self.rate=rate

  def forward(self,x):
    if self.training:
      z = np.random.binomial(size=(1,x.shape[1]), n=1, p= 1-self.rate)*(1./(1.-self.rate))
      x=x*z
    return x

############################ losses
class LayerLoss(Layer):
    def __init__(self):
      super().__init__()

    def set_truth(self,truth):
      self.truth = truth

class LossMSE(LayerLoss): #mean square error
  def __init__(self):
    super().__init__()

  def forward(self,x):
    d = x - self.truth
    if self.training:
      self.dydx = d
    return d * d * 0.5

class LossMAE(LayerLoss): #mean absolute error
  def __init__(self):
    super().__init__()

  def forward(self,x):
    d = x - self.truth
    if self.training:
      self.dydx = np.sign(d)
    return np.abs(d)

class LossLogCosh(LayerLoss):
  def __init__(self):
    super().__init__()

  def forward(self,x):
    d = x - self.truth
    if self.training:
      self.dydx = np.tanh(d)
    return  np.log(np.cosh(d))

# see https://ml-cheatsheet.readthedocs.io/en/latest/loss_functions.html
class LossBinaryCrossEntropy(LayerLoss):
  def __init__(self):
    super().__init__()

  def forward(self,x): #todo test x.size ==1
    p=x
    t=self.truth
    if self.training:
      self.dydx = np.atleast_2d(-t/np.maximum(1.e-8,p) +(1.-t)/np.maximum(1.e-8,1.-p) )
    return  np.atleast_2d(-t*np.log(np.maximum(p,1.e-8)) -(1.-t)*np.log(np.maximum(1.e-8,1.-p)))

# see https://gombru.github.io/2018/05/23/cross_entropy_loss
class LossCrossEntropy(LayerLoss):
  def __init__(self):
    super().__init__()

  def forward(self,x):
    p_max=np.maximum(x,1.e-8)
    t=self.truth
    if self.training:
      ump_max=np.maximum(1.-x,1.e-8)
      self.dydx = -t/p_max+(1.-t)/ump_max
    return  np.atleast_2d(np.mean(-t*np.log(p_max),axis=1))

class LossSparseCategoricalCrossEntropy(LayerLoss):
  def __init__(self):
    super().__init__()

  def forward(self,x):
    p_max=np.maximum(x,1.e-8)
    t=to_one_hot(self.truth,x.shape[1]) # todo optimize
    if self.training:
      ump_max=np.maximum(1.-x,1.e-8)
      self.dydx = -t/p_max+(1.-t)/ump_max
    return  np.atleast_2d(np.mean(-t*np.log(p_max),axis=1))

###################################################################################################
class Net:
  classification_mode=True
  def __init__(self):
    self.layers = list()

  def set_classification_mode(self,bClassification):
    self.classification_mode=bClassification

  def append(self,layer):
    self.layers.append(layer)

  def predict(self,x):
    out = x
    for l in self.layers:
      out = l.forward(out)

    if self.classification_mode:
      if out.shape[1]>1:
        out=np.argmax(out,axis=1)
      else:
        out=np.around(out)

      out=out.astype(int)
    return out
###################################################################################################
class NetTrain:
  optim = opt.OptimizerMomentum()
  loss_layer = None
  epochs = 100
  batch_size = 32
  test_data=None
  test_truth=None
  log_console=False
  epoch_callback=None
  current_train_accuracy=-1

  #keep best parameters
  keep_best=True
  best_net=None
  best_accuracy=0

  def get_current_train_accuracy(self):
      return self.current_train_accuracy

  def set_loss(self,layerloss):
    self.loss_layer = layerloss

  def set_optimizer(self, optim):
    self.optim = optim

  def set_test_data(self,test_data,test_truth):
    self.test_data=test_data
    self.test_truth=test_truth

  def set_keep_best(self,keep_best):
    self.keep_best=keep_best

  def set_epoch_callback(self,epoch_callback):
    self.epoch_callback=epoch_callback

  def forward_and_backward(self,x,t):
    #forward pass
    out = x
    for l in self.n.layers:
      out = l.forward(out)

	#backward pass
    self.loss_layer.set_truth(t)
    online_loss = self.loss_layer.forward(out)

    #update and back propagate gradient with loss
    dldy = self.loss_layer.dydx
    for i in range(len(self.n.layers) - 1,-1,-1):
      l = self.n.layers[i]
      dldy = l.backpropagation(dldy)

    return online_loss

  def fit(self,n,train_data,train_truth):

    #reshape truth to 2 ndim if 1D
    if train_truth.ndim==1:
      train_truth=np.expand_dims(train_truth, axis=1)

    self.n = n
    self.best_net= copy.deepcopy(n)
    nblayer = len(n.layers)
    nbsamples = len(train_data)
    self.epoch_loss = np.zeros((0,0),dtype=np.float32)

    #init optimizer
    optiml = list()
    for i in range(nblayer*2):
      optiml.append(copy.copy(self.optim)) # reserve weight optimizer
      optiml.append(copy.copy(self.optim)) # reserve bias optimizer

    #init layers
    for l in n.layers:
      l.init()

    if self.batch_size == 0:
      self.batch_size = nbsamples

    #train!
    for epoch in range(self.epochs):
      if self.log_console:
        print("Epoch: ",epoch+1,"/",self.epochs,sep='', end='')

      #shuffle data
      perm = np.random.permutation(nbsamples)
      s2 = train_data[perm,:]
      t2 = train_truth[perm,:]
      sumloss = 0.

      #set layers in train mode
      self.loss_layer.training=True
      for l in n.layers:
        l.training=True

      batch_start = 0
      while batch_start < nbsamples:

        #prepare batch data
        batch_end = min(batch_start + self.batch_size,nbsamples)
        x1 = s2[batch_start:batch_end,:]
        out = t2[batch_start:batch_end,:]
        batch_start += self.batch_size

        #forward pass and gradient computation
        online_loss = self.forward_and_backward(x1,out)

        # weights and bias optimization
        for i in range(len(n.layers)):
          l = n.layers[i]
          if l.learnable:
            l.w = optiml[2*i].optimize(l.w,l.dldw)

          if l.learnBias:
            l.b = optiml[2*i+1].optimize(l.b,l.dldb)

        #save stats
        sumloss+=online_loss.sum()

      #set layers in test mode
      self.loss_layer.training=False
      for l in n.layers:
        l.training=False

      self.epoch_loss = np.append(self.epoch_loss,sumloss / nbsamples)

      #compute train accuracy (for now: classification only)
      predicted = n.predict(train_data)

      # classification case
      if(n.classification_mode==True):
        accuracy=100.*np.mean(predicted==train_truth.ravel())
        if self.log_console:
          print(" Train Accuracy: "  +format(accuracy, ".2f")  + "%", end='')

        #compute test accuracy (for now: classification only)
        if self.test_data is not None:
          predicted = n.predict(self.test_data)
          accuracy=100.*np.mean(predicted==self.test_truth.ravel())
          if self.log_console:
            print(" Test Accuracy: "+format(accuracy, ".2f")+"%", end='')

        self.current_train_accuracy=accuracy

        if(self.best_accuracy<accuracy):
          self.best_net=copy.deepcopy(n)
          self.best_accuracy=accuracy
          if self.log_console:
            print(" (new best accuracy)",end='')

        if self.log_console:
          print("")

      if self.epoch_callback!=None:
        self.epoch_callback(self)

    # if self.keep_best and (self.best_accuracy!=0):
    #   n=copy.deepcopy(self.best_net)


# Question 07(b), Lab 04
# AB Satyaprakash - 180123062

# imports ----------------------------------------------------------------------------
import random
import numpy as np
import matplotlib.pyplot as plt

# functions --------------------------------------------------------------------------

def f(x):
    return (x**3) + (0.01*random.random())

def compositeTrapezoidalRule(X):
    sum = 0
    a, b = X[0], X[-1]
    n = X.shape[0]
    h = (b-a)/(n-1)
    for i in range(n):
        x = X[i]
        if(i == 0 or i == n-1):
            sum += f(x)/2
        else:
            sum += f(x)
    return (h*sum)

# program body
errorArray = []
for i in range(30, 5000):
    h = (1-0)/i
    X = np.arange(0, 1+h/2, h)
    errorArray.append(abs(0.25-(compositeTrapezoidalRule(X))))

h = 1/5000
X = np.arange(0, 1+h/2, h)
print('Using inexact function evaluations')
print('Approximate value of integral for h=0.0002 is', compositeTrapezoidalRule(X))
print('Error in the process:', abs(0.25-compositeTrapezoidalRule(X)))

plt.title("Error vs n")
plt.xlabel("n")
plt.ylabel("error in estimation")
plt.plot(range(30, 5000), errorArray)
plt.xticks(range(30, 5000, 500))
plt.show()

Semester 6/MA 322 (Scientific Computing Theory)/Labs/Lab 4/Code/q7.py


"""
Househlolder Triangularization
This algorithm uses a projection idea to rotate vectors
(from each diagonal element down) to align with a coordinate axis,
so the length (norm) is unchanged, but zeros are produced below the
diagonal element resulting in an upper triangular matrix.
Each rotation is a multiplication by an orthogonal matrix
Q can be produced if needed using get_q(V).  But often it is enough to
compute Q*b or Qx so methods are provided for those operations.
Most production QR algorithms use this approach

>>> A = array([[-2.49952922, -1.03751133, -1.21085266, -1.24115004],
...            [-0.80586535,  1.31580494, -0.25698657, -0.09300651],
...            [-0.31995424, -0.36042199, -1.18012579, -0.12960803],
...            [-0.51933797, -0.29275398,  0.0953217 , -0.32097443],
...            [-0.66819142,  0.18847333, -0.43734184, -0.30990116]])
>>> V,R = hqr(A)
>>> linalg.norm(tril(R,-1),'f') < 1.0e-15
True
>>> Q = makeq(V)
>>> linalg.norm(A - Q.dot(R),'f') < 1.0e-15
True
>>> A = array([[ 0.04+0.46j,  0.02+0.61j,  0.55+0.87j],
...            [ 0.94+0.69j,  1.00+0.94j,  0.17+0.24j],
...            [ 0.07+0.52j,  0.48+0.48j,  0.07+0.49j]])
>>> V,R = hqr(A)
>>> linalg.norm(tril(R,-1),'f') < 1.0e-15
True
>>> Q = makeq(V)
>>> linalg.norm(A - Q.dot(R),'f') < 2.0e-15
True
"""

from numpy import *

def inner(v,w):
    return v.conj().T.dot(w)

def sgn(x):
    """
    To handle the complex case, we need to
    use this definition of the sign(A) instead of numpy.sign,
    which just takes the sign of the real part.
    """
    return 1 if x == 0 else x/abs(x)

def hqr(Ao):
    """
    Given a matrix, compute the Householder triangularization
    """
    A = Ao.copy()
    tp = A.dtype
    m,n = shape(A)
    V = zeros((m,n), dtype=tp)
    for k in range(n):
        x = A[k:,k].copy()
        nx = linalg.norm(x,2)
        x[0] += sgn(x[0])*linalg.norm(x,2)
        vk = x[:,None] / linalg.norm(x,2) #norm of changed x
        V[k:,k] = vk[:,0]
        # subtract an (m-k+1)x(m-k+1) rank 1 submatrix from A
        A[k:,k:] -= 2*vk.dot(inner(vk, A[k:,k:]))
    return V,A

def qhb(V,bo):
    """
    Given the matrix V returned from hqr and an m-vector b
    compute the product Q* b
    """
    _,n = shape(V)
    b = bo.copy()
    for k in range(n):
        b[k:] -= 2*V[k:,k]*(V[k:,k].conj().dot(b[k:]))
    return b

def qx(V,xo):
    """
    Given the matrix V returned from hqr and a vector
    x with m rows, compute the product Q x
    """
    _,n = shape(V)
    x = xo.copy()
    for k in range(n-1,-1,-1):
        #x[k:] -= 2*V[k:,k]*(V[k:,k].conj().dot(x[k:]))
        x[k:] -= 2*V[k:,k]*(inner(V[k:,k], x[k:]))
    return x

def makeq1(V):
    """
    Given the matrix V returned from hqr, form the matrix Q by
    computing Q* I and taking the conjugate transpose.
    """
    tp = V.dtype
    m,_ = shape(V)
    Q = zeros((m,m),dtype=tp)
    I = eye(m,dtype=tp)
    for i in range(m):
        Q[:,i]=qhb(V,I[:,i])
    return Q.conj().T

def makeq(V):
    """
    Given the matrix V returned from hqr, form the matrix Q by
    computing Q I
    """
    tp = V.dtype
    m,_ = shape(V)
    Q = zeros((m,m),dtype=tp)
    I = eye(m,dtype=tp)
    for i in range(m):
        Q[:,i]=qx(V,I[:,i])
    return Q

def bslv(R,c):
    """
    R should be upper triangular mxn with nonzero diagonal entries
    and c mx1.  Solves rx = c by back substitution
    """
    tp = R.dtype
    _,n = shape(R)
    x = zeros(n,dtype=tp)
    for i in range(n-1,-1,-1):
        x[i] = (c[i]-R[i,i+1:].dot(x[i+1:])) / R[i,i]
    return x

def slv(A,b):
    V,R = hqr(A)
    c = qhb(V,b)
    x = bslv(R,c)
    return x

if __name__ == "__main__":
    import doctest
    doctest.testmod()


from math import sqrt
import numpy as np
from scipy.stats import pearsonr
from scipy.stats import spearmanr

def RMSE(act, pred):
    '''
    accept two numpy arrays
    '''
    return sqrt(np.mean(np.square(act - pred)))

def Pearson(act, pred):
    return pearsonr(act, pred)[0]

def Spearman(act, pred):
    '''
    Note: there is no need to use spearman correlation for now
    '''
    return spearmanr(act, pred)[0]

def Delta_t95(act, pred):
    num95 = int(np.ceil(len(act) * 0.95))
    return 2 * sorted(abs(act - pred))[num95 - 1]

def Delta_tr95(act, pred):
    return Delta_t95(act, pred) / (max(act) - min(act))


import numpy as np
import math
from timeit import default_timer as timer

#Computes the function
def f(x,y):
    return 100*(y-x**2)**2+(1-x)**2

#The gradient of the function
def gradf(x,y):
    gx=2*(200*x**3-200*x*y+x-1)
    gy=200*(y-x**2)
    return np.array([gx,gy])

#Golden search method
def golden_search(a, d,delta, pos, direction):
    #initializing parameters
    k=1
    continueGS=True
    xstart=pos[0]
    ystart=pos[1]
    d1=direction[0]
    d2=direction[1]
    phi=((-1+math.sqrt(5))/2)

    #algorithm of golden search
    while continueGS:

        #ratio of separation
        step=phi*(d-a)

        #images of intermediate points
        f1=f(xstart+(step+a)*d1,ystart+(step+a)*d2)
        f2=f(xstart+(d-step)*d1,ystart+(d-step)*d2)

        #updating searching section
        if f2<f1:
            a=a
            d=(step+a)

        else:
            d=d
            a=(d-step)

        #termination condition
        if (abs(d-a))<delta:
            search_magnitude=(a+d)/2
            continueGS=False
        else:
            k+=1

    return search_magnitude

#scalar product of two vectors
def scalarprod(xv,yv):
    prod=0
    for i in range(2):
        prod=prod+xv[i]*yv[i]
    return prod

#step function for the conjugate gradient method
def s(scalar,v,x0):
    vv=v
    xs=np.zeros(len(vv))
    for i in range(len(vv)):
        vv[i]=vv[i]*scalar
        xs[i]=x0[i]+vv[i]
    return xs

#main function or Conjugate Gradient Method
def cgm(xseed,yseed,len_uncert,delta,maxiter=1e6):
    #initializing some  variables
    itera=0
    start = timer()
    points=np.zeros((int(maxiter+2),2))
    Xk=np.array([xseed,yseed])
    Alg_end=True
    sizek=-gradf(Xk[0],Xk[1])

    #main while
    while Alg_end==True:
        itera=itera+1

        #previous computation of gradient
        grad=gradf(Xk[0],Xk[1])

        #Compute the step size(alpha) using golden search
        alpha=golden_search(-2,2,delta,Xk,sizek)

        #making the step
        sizecopy=np.array([sizek[0],sizek[1]])
        Xk1=s(alpha,sizecopy,Xk)

        #calculating the beta(Fletcher-Reeves)
        newgrad=gradf(Xk1[0],Xk1[1])
        betak=scalarprod(gradf(Xk1[0],Xk1[1]),gradf(Xk1[0],Xk1[1]))/scalarprod(gradf(Xk[0],Xk[1]),gradf(Xk[0],Xk[1]))

        #saving points
        x=float(Xk1[0])
        y=float(Xk1[1])
        points[itera,0]=x
        points[itera,1]=y

        #final orthogonal direction
        sizek=-gradf(Xk1[0],Xk1[1])+betak*sizek

        #termination condition
        if np.sqrt((newgrad[0])**2+(newgrad[1])**2)<len_uncert or itera>maxiter:
            Alg_end=False
            if itera>maxiter:
                print("point reached by maxiter\n")
            if np.sqrt((Xk[0]-Xk1[0])**2+(Xk[1]-Xk1[1])**2)<len_uncert:
                print("point reached by epsilon condition\n")

        #updating position
        Xk=Xk1

    #some calculations for performance
    end = timer()
    timef=end-start
    print(end - start)
    print("Algorithm finished with",itera,"iterations and a time of ",timef,"seconds", "\nthe final point is (",Xk[0],Xk[1],")")
    return itera,timef,points

a,t,pointsv=cgm(-1.5,-1,1e-9,1e-8)
#uncomment this last two lines to get the points and a csv with the points of the algorithm
print(pointsv)
np.savetxt("pointsCGM.csv",pointsv,delimiter=",")


import numpy as np

def gaussianKernel(x1, x2, sigma):
    """returns a gaussian kernel between x1 and x2
    and returns the value in sim
    """

    # Ensure that x1 and x2 are column vectors
    #     x1 = x1.ravel()
    #     x2 = x2.ravel()
    if x1.ndim == 1:
        x1 = x1.reshape(-1, 1)
    if x2.ndim == 1:
        x2 = x2.reshape(-1, 1)

    # ====================== YOUR CODE HERE ======================
    # Instructions: Fill in this function to return the similarity between x1
    #               and x2 computed using a Gaussian kernel with bandwidth
    #               sigma
    #
    gamma = 1 / (2 * sigma**2)
    sim = np.exp(-gamma * (x1 - x2).T @ (x1 - x2))
    # =============================================================

    return sim


import numpy as np
from scipy.stats import geom, norm
import math

'''
sample from geometric distribution
@:param e the expectation of geometric distribution
@:param N the number of data point to be sampled and calculate average
'''
def sample_from_geometric_distribution(e, N):

    # geometry distribution parameter
    p = 1.0 / e

    while True:

        # sample N bids from geometric distribution
        r = geom.rvs(p, size=N)

        # calculate the average of all N sampled data point
        feedback = math.floor(r.mean())

        # ensure that feedback is positive, or sample feedback again
        if feedback > 0:
           break

    return feedback

'''
sample from norm distribution
@:param loc the expectation of norm distribution
@:param scale the scale of norm distribution
'''
def sample_from_norm_distribution(loc, scale):

    return norm.rvs(loc=loc, scale=scale, size=1)[0]

# the main function to test
if __name__ == '__main__':

    print(sample_from_geometric_distribution(1000000, 10))
    print(sample_from_norm_distribution(0, 1))


import numpy as np
import matplotlib.pyplot as plt

def estimate_pi(n, plot=False):
    pts = np.random.random((n, 2))

    norm = np.linalg.norm(pts, axis=-1)
    frac_in_circle = np.average(norm <= 1)
    pi_est = frac_in_circle * 4

    if plot:
        plt.plot(pts[:, 0], pts[:, 1], ',')

        x = np.linspace(0, 1, 100)
        y = np.sqrt(1-x**2)
        plt.plot(x, y, 'g-')
        plt.fill_between(x, 0, y, facecolor='g', alpha=0.2)
        plt.title("Monte Carlo Estimate of Pi with {} points: {}".format(n, pi_est))

        plt.axis('equal')
        plt.tight_layout()
        plt.show()
    return pi_est

if __name__ == "__main__":
    n = int(input("Input a number of points to sample: "))
    estimate_pi(n, plot=True)


import numpy as np

# Write a function that takes as input a list of numbers, and returns
# the list of values given by the softmax function.

def softmax(L):
    denominator = 0
    for item in L:
        denominator += np.e**item
    results = []
    for item in L:
        prob_item = np.e**item / denominator
        results.append(prob_item)

    return results

if __name__ == "__main__":
    softmax([5, 6, 7])
    # [0.09003057317038046, 0.24472847105479764, 0.6652409557748219]


import numpy as np

def myPCA(X,k):
    #standardize the dataset
    mu = np.mean(X, axis = 0)
    X_std = X - mu
   #X_std = X_std.transpose()

   # PCA with a covariance matrix
    #covariance and maximum eigenvalue

    covariance_matrix = np.cov(X_std, rowvar = False)
    eig_vals, eig_vecs = np.linalg.eig(covariance_matrix)

    sorted_components = np.argsort(eig_vals)[::-1]

    sorted_eig_vecs = eig_vecs[sorted_components]

    W = sorted_eig_vecs[:,0:k]
    return W,mu

def ProjectDatapoints(X,W,mu):
    X_std = X - mu

   # projected data
    X_new = np.dot(W.transpose(), X_std.transpose()).transpose()
    return X_new