from Analyze.AnalyzeModel import *
from Analyze.Filters import *
from Drivers.HistogramModelDriver import *

import numpy as np
import numba
import random

class ImageModelDriver:

    # -------------------------------
    # Лекция 1
    # -------------------------------

    @staticmethod
    def multi_shift(data: SPDImage, c):
        result_array = []

        for row in data.modified_image:
            y_array = [y * c for y in row]
            result_array.append(y_array)

        data.update(np.array(result_array), '_multi_shift')

    @staticmethod
    def add_shift(data: SPDImage, c):
        result_array = []

        for row in data.modified_image:
            y_array = [y + c for y in row]
            result_array.append(y_array)

        data.update(np.array(result_array), '_add_shift')

    @staticmethod
    def grayscale(data: SPDImage, dtype=np.uint8):
        max_a = np.max(data.modified_image)
        min_a = np.min(data.modified_image)
        diff_a = max_a - min_a
        result_array = []

        for row in data.modified_image:
            y_array = [(y - min_a) * np.iinfo(dtype).max / diff_a for y in row]
            result_array.append(y_array)

        data.update(np.array(result_array), '_grayscale')

    # -------------------------------
    # Лекция 2
    # -------------------------------

    @staticmethod
    def rotate90(data: SPDImage):
        data.update(np.rot90(data.modified_image), '_rotated')

    @staticmethod
    def resize(data: SPDImage, resample: int, scale_const: float, dtype=np.uint8):
        resize_height = int(data.modified_image.shape[0] * scale_const)
        resize_width = int(data.modified_image.shape[1] * scale_const)

        image_from_array = Image.fromarray(data.modified_image.astype(dtype))
        resized_image = image_from_array.resize((resize_width, resize_height), resample)

        if resample == Image.NEAREST:
            resample_type_text = "nearest"
        elif resample == Image.BOX:
            resample_type_text = "box"
        elif resample == Image.BILINEAR:
            resample_type_text = "bilinear"
        else:
            resample_type_text = str(resample)

        data.update(np.array(resized_image, copy=True), '_resized_' + resample_type_text)

    # -------------------------------
    # Лекция 3
    # -------------------------------

    @staticmethod
    def negative(data: SPDImage):
        max_a = np.max(data.modified_image)
        result_array = []

        for row in data.modified_image:
            y_array = [max_a - 1 - y for y in row]
            result_array.append(y_array)

        data.update(np.array(result_array), '_negative')

    @staticmethod
    def gamma_correction(data: SPDImage, const, gamma):
        result_array = []

        for row in data.modified_image:
            y_array = np.array([const * (np.power(y, gamma)) for y in row])
            result_array.append(y_array)

        data.update(np.array(result_array), '_gamma_correction')

    # Делаем изображение более контрастным при увеличении const
    # Делаем изображение менее констрастным при уменьшении const
    @staticmethod
    def log_correction(data: SPDImage, const):
        result_array = []

        for row in data.modified_image:
            y_array = [const * (np.log(y + 1)) for y in row]
            result_array.append(y_array)

        data.update(np.array(result_array), '_log_correction')

    # -------------------------------
    # Лекция 5
    # -------------------------------

    @staticmethod
    @numba.jit(nopython=True)
    def convolution(main_data: np.ndarray, add_data: np.ndarray, dt: float):
        x_array = []
        y_array = []

        for i in range(len(main_data) + len(add_data)):
            sum = 0

            for j in range(len(add_data)):
                if 0 <= (i - j) < len(main_data):
                    sum += main_data[i - j] * add_data[j]
                else:
                    sum += 0

            if (len(add_data) / 2) <= i < (len(main_data) + len(add_data) / 2):
                y_array.append(sum)
                x_array.append((i - len(add_data) / 2) * dt)

        return [x_array, y_array]

    @staticmethod
    def find_moire_for_image(image: SPDImage, m, quantile=0.7, dt=1):
        diff_im = []
        for row_idx in range(0, len(image.modified_image), m):
            row = image.modified_image[row_idx]
            diff_im.append(AnalyzeModel.diff(row))

        max_auto_freq = []
        for diff in diff_im:
            auto = AnalyzeModel.auto_corr_array(diff)
            auto_fourier = AnalyzeModel.amplitude(auto, dt)
            max_auto_fourier_freq_idx = np.argmax(auto_fourier[1])
            max_auto_fourier_freq = auto_fourier[0][max_auto_fourier_freq_idx] // 0.01 * 0.01
            max_auto_freq.append(max_auto_fourier_freq)

        max_mutal_freq = []
        for row_idx in range(0, len(diff_im)):
            row_f = diff_im[row_idx]
            row_s = diff_im[(row_idx + 1) % len(diff_im)]
            mutal = AnalyzeModel.mutual_corr_array(row_f, row_s)
            mutal_fourier = AnalyzeModel.amplitude(mutal, dt)
            max_mutal_fourier_freq_idx = np.argmax(mutal_fourier[1])
            max_mutal_fourier_freq = mutal_fourier[0][max_mutal_fourier_freq_idx] // 0.01 * 0.01
            max_mutal_freq.append(max_mutal_fourier_freq)

        all_freqs = max_auto_freq + max_mutal_freq
        freqs_counts = {}

        for freq in all_freqs:
            if freq in freqs_counts:
                freqs_counts[freq] += 1
            else:
                freqs_counts[freq] = 1

        result_freqs = []

        for key in freqs_counts:
            if (freqs_counts[key] / len(all_freqs)) >= quantile:
                result_freqs.append(key)

        return result_freqs

    @staticmethod
    def fix_moire_for_image(
            image: SPDImage,
            result_freqs,
            apply_vertical_fix,
            shift,
            m
    ) -> bool:
        if len(result_freqs) == 0:
            print('Не нашлась решетка на изображении!')
            return False

        first_top_freq = result_freqs[0]
        filter = Filters.bsw_filter(first_top_freq - shift, first_top_freq + shift, 1, m)[1]

        print('Работаем с частотой: ' + str(first_top_freq))

        result = []
        for row in image.modified_image:
            conv = ImageModelDriver.convolution(np.array(row), np.array(filter), 1)[1]
            result.append(conv)

        if apply_vertical_fix:
            rotated = np.rot90(result)
            result = []
            for row in rotated:
                conv = ImageModelDriver.convolution(np.array(row), np.array(filter), 1)[1]
                result.append(conv)
            image.update(np.rot90(result, k=3), '_fixed')
        else:
            image.update(result, '_fixed')

        print('Изображение ' + image.name + ' исправлено!\n')

        return True

    # -------------------------------
    # Лекция 6
    # -------------------------------

    @staticmethod
    def salt_pepper(image: SPDImage, dots_count: int):
        final_image = []

        for row in image.modified_image:
            result = ImageModelDriver.salt_pepper_per_line(row, dots_count)
            final_image.append(result)

        image.update(final_image, '_salt_and_pepper')

    @staticmethod
    @numba.jit(nopython=True)
    def salt_pepper_per_line(image_data: np.ndarray, dots_count: int) -> np.ndarray:
        result_row = []

        for item in image_data:
            possibility = random.randint(0, 1000)

            if possibility < dots_count:
                result_row.append(0)
            elif 300 < possibility < (300 + dots_count):
                result_row.append(255)
            else:
                result_row.append(item)

        return np.array(result_row)

    @staticmethod
    def random_noize(image: SPDImage, intensity: int):
        final_image = []

        for row in image.modified_image:
            result = ImageModelDriver.random_noize_per_line(row, intensity)
            final_image.append(result)

        image.update(final_image, '_random_noize')

    @staticmethod
    @numba.jit(nopython=True)
    def random_noize_per_line(image_data: np.ndarray, intensity: int) -> np.ndarray:
        result_row = []

        for item in image_data:
            noize = random.randint(0, intensity)
            sign = random.randint(0, 1000)

            if sign > 500:
                result_row.append((item + noize) % 255)
            else:
                result_row.append((item - noize) % 255)
        return np.array(result_row)

    @staticmethod
    def linear_filter(image_sd: SPDImage, kernel_count: int):
        image = image_sd.modified_image
        kernel = np.ones([kernel_count, kernel_count], dtype=int)

        image_row, image_col = image.shape
        kernel_row, kernel_col = kernel.shape

        output = np.zeros(image.shape)

        pad_height = int((kernel_row - 1) / 2)
        pad_width = int((kernel_col - 1) / 2)

        padded_image = np.zeros((image_row + (2 * pad_height), image_col + (2 * pad_width)))

        padded_image[pad_height:padded_image.shape[0] - pad_height, pad_width:padded_image.shape[1] - pad_width] = image

        for row in range(image_row):
            for col in range(image_col):
                output[row, col] = np.mean(kernel * padded_image[row:row + kernel_row, col:col + kernel_col])

        processed_image = np.array(output)

        image_sd.update(processed_image, '_linear_filter')

    @staticmethod
    def median_filter(image_sd: SPDImage, kernel_count: int):
        image = image_sd.modified_image
        kernel = np.ones([kernel_count, kernel_count], dtype=int)

        image_row, image_col = image.shape
        kernel_row, kernel_col = kernel.shape

        output = np.zeros(image.shape)

        pad_height = int((kernel_row - 1) / 2)
        pad_width = int((kernel_col - 1) / 2)

        padded_image = np.zeros((image_row + (2 * pad_height), image_col + (2 * pad_width)))

        padded_image[pad_height:padded_image.shape[0] - pad_height, pad_width:padded_image.shape[1] - pad_width] = image

        for row in range(image_row):
            for col in range(image_col):
                output[row, col] = np.median(kernel * padded_image[row:row + kernel_row, col:col + kernel_col])

        processed_image = np.array(output)

        image_sd.update(processed_image, '_median_filter')

    # -------------------------------
    # Лекция 9
    # -------------------------------

    @staticmethod
    def blur_fix(image_sd: SPDImage, kernel: np.ndarray, alpha: float):
        kernel = np.pad(
            kernel, [
                (0, image_sd.modified_image.shape[0] - kernel.shape[0]),
                (0, image_sd.modified_image.shape[1] - kernel.shape[1])
            ]
        )

        kernel_f = AnalyzeModel.two_d_fourier(kernel, 1)
        image_sd_f = AnalyzeModel.two_d_fourier(image_sd.modified_image, 1)

        diff = (np.conj(kernel_f.complex_data) / (np.abs(kernel_f.complex_data) ** 2 + alpha * alpha)) * image_sd_f.complex_data
        diff_f = Fourier(diff, 1).two_d_back_transform()

        image_sd.modified_folder = image_sd.modified_folder + image_sd.name + '/'
        image_sd.update(diff_f, '_back_two_d_fourier')

        ImageModelDriver.grayscale(image_sd)

    # -------------------------------
    # Лекция 9
    # -------------------------------

    @staticmethod
    def high_pass_filter(image_sd: SPDImage, type: str, filter_radius):
        image_sd_f = AnalyzeModel.two_d_fourier(image_sd.modified_image, 1)
        img = np.fft.fftshift(image_sd_f.complex_data)

        height, weight = img.shape
        center_h = int(height / 2)
        center_w = int(weight / 2)

        if type == 'ideal':
            high_pass_filter = np.ones_like(img)

            for i in range(height):
                for j in range(weight):
                    dist_from_center = np.sqrt((i - center_h) ** 2 + (j - center_w) ** 2)
                    if dist_from_center < filter_radius:
                        high_pass_filter[i][j] = 0

        elif type == 'gausse':
            high_pass_filter = np.zeros_like(img)

            for i in range(height):
                for j in range(weight):
                    dist_from_center = np.sqrt((i - center_h) ** 2 + (j - center_w) ** 2)
                    high_pass_filter[i][j] = 1 - np.exp(-(dist_from_center ** 2) / (2 * filter_radius ** 2))
        else:
            return

        result_f = Fourier(np.fft.ifftshift(img * high_pass_filter), image_sd_f.dt)
        new_image = result_f.two_d_back_transform()

        image_sd.modified_folder = image_sd.modified_folder + '_hpf_' + type + '_' + str(filter_radius) + '/'
        image_sd.update(new_image, '_hpf_' + type)
        ImageModelDriver.grayscale(image_sd)

    @staticmethod
    def low_pass_filter(image_sd: SPDImage, type: str, filter_radius):
        image_sd_f = AnalyzeModel.two_d_fourier(image_sd.modified_image, 1)
        img = np.fft.fftshift(image_sd_f.complex_data)

        height, weight = img.shape
        center_h = int(height / 2)
        center_w = int(weight / 2)

        if type == 'ideal':
            low_pass_filter = np.zeros_like(img)

            for i in range(height):
                for j in range(weight):
                    dist_from_center = np.sqrt((i - center_h) ** 2 + (j - center_w) ** 2)
                    if dist_from_center < filter_radius:
                        low_pass_filter[i][j] = 1

        elif type == 'gausse':
            low_pass_filter = np.zeros_like(img)

            for i in range(height):
                for j in range(weight):
                    dist_from_center = np.sqrt((i - center_h) ** 2 + (j - center_w) ** 2)
                    low_pass_filter[i][j] = np.exp(-(dist_from_center ** 2) / (2 * filter_radius ** 2))
        else:
            return

        result_f = Fourier(np.fft.ifftshift(img * low_pass_filter), image_sd_f.dt)
        new_image = result_f.two_d_back_transform()

        image_sd.modified_folder = image_sd.modified_folder + '_lpf_' + type + '_' + str(filter_radius) + '/'
        image_sd.update(new_image, '_lpf_' + type)
        ImageModelDriver.grayscale(image_sd)

    @staticmethod
    def threshold(image_sd: SPDImage, threshold_const=127):
        image = image_sd.modified_image

        image[image > threshold_const] = np.iinfo(image_sd.dtype).max
        image[image != np.iinfo(image_sd.dtype).max] = np.iinfo(image_sd.dtype).min

        image_sd.update(image, '_threshold_' + str(threshold_const))

    # -------------------------------
    # Лекция 10
    # -------------------------------

    @staticmethod
    def simple_gradient(image_sd: SPDImage):
        mask_x = np.array([
            [-1, -1, -1], [0, 0, 0], [1, 1, 1],
        ])

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

        processed_image_x = ImageModelDriver.convolution_2d(image_sd, mask_x)
        processed_image_y = ImageModelDriver.convolution_2d(image_sd, mask_y)
        processed_image = np.sqrt(np.square(processed_image_x) + np.square(processed_image_y))

        image_sd.modified_folder = image_sd.modified_folder + '_simple_gradient_filter/'
        image_sd.update(processed_image, '_simple_gradient_filter')

    @staticmethod
    def simple_gradient_x(image_sd: SPDImage):
        mask = np.array([
            [-1, -1, -1], [0, 0, 0], [1, 1, 1],
        ])

        processed_image = ImageModelDriver.convolution_2d(image_sd, mask)

        image_sd.modified_folder = image_sd.modified_folder + '_simple_gradient_x_filter' + '/'
        image_sd.update(processed_image, '_simple_gradient_x_filter')

    @staticmethod
    def simple_gradient_y(image_sd: SPDImage):
        mask = np.array([
            [-1, 0, 1], [-1, 0, 1], [-1, 0, 1]
        ])

        processed_image = ImageModelDriver.convolution_2d(image_sd, mask)

        image_sd.modified_folder = image_sd.modified_folder + '_simple_gradient_y_filter' + '/'
        image_sd.update(processed_image, '_simple_gradient_y_filter')

    @staticmethod
    def sobel_gradient(image_sd: SPDImage):
        mask_x = np.array([
            [-1, -2, -1], [0, 0, 0], [1, 2, 1],
        ])

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

        processed_image_x = ImageModelDriver.convolution_2d(image_sd, mask_x)
        processed_image_y = ImageModelDriver.convolution_2d(image_sd, mask_y)
        processed_image = np.sqrt(np.square(processed_image_x) + np.square(processed_image_y))

        image_sd.modified_folder = image_sd.modified_folder + '_sobel_gradient_filter' + '/'
        image_sd.update(processed_image, '_sobel_gradient_filter')

    @staticmethod
    def sobel_gradient_x(image_sd: SPDImage):
        mask = np.array([
            [-1, -2, -1], [0, 0, 0], [1, 2, 1],
        ])

        processed_image = ImageModelDriver.convolution_2d(image_sd, mask)

        image_sd.modified_folder = image_sd.modified_folder + '_sobel_gradient_x_filter' + '/'
        image_sd.update(processed_image, '_sobel_gradient_x_filter')

    @staticmethod
    def sobel_gradient_y(image_sd: SPDImage):
        mask = np.array([
            [-1, 0, 1], [-2, 0, 2], [-1, 0, 1]
        ])

        processed_image = ImageModelDriver.convolution_2d(image_sd, mask)

        image_sd.modified_folder = image_sd.modified_folder + '_sobel_gradient_y_filter' + '/'
        image_sd.update(processed_image, '_sobel_gradient_y_filter')

    @staticmethod
    def laplas_gradient(image_sd: SPDImage):
        mask = np.array([
            [-1, -1, -1], [-1, 8, -1], [-1, -1, -1]
        ])

        processed_image = ImageModelDriver.convolution_2d(image_sd, mask)

        ShF = 100
        processed_image = processed_image * ShF / np.amax(processed_image)

        image_sd.modified_folder = image_sd.modified_folder + '_laplas_gradient_filter' + '/'
        image_sd.update(processed_image, '_laplas_gradient_filter')

    @staticmethod
    def convolution_2d(image_sd: SPDImage, kernel):
        image = image_sd.modified_image
        image_row, image_col = image.shape
        kernel_row, kernel_col = kernel.shape

        output = np.zeros_like(image)

        pad_height = int((kernel_row - 1) / 2)
        pad_width = int((kernel_col - 1) / 2)

        padded_image = np.zeros((image_row + (2 * pad_height), image_col + (2 * pad_width)))

        padded_image[pad_height:-pad_height, pad_width:-pad_width] = image

        for row in range(image_row):
            for col in range(image_col):
                output[row, col] = np.sum(kernel * padded_image[row:row + kernel_row, col:col + kernel_col])

        return output

    @staticmethod
    def dilate(image_sd: SPDImage, dilation_level=3):
        dilation_level = 3 if dilation_level < 3 else dilation_level

        structuring_kernel = np.full(shape=(dilation_level, dilation_level), fill_value=255)
        image_src = image_sd.modified_image

        orig_shape = image_src.shape
        pad_width = dilation_level - 2

        image_pad = np.pad(array=image_src, pad_width=pad_width, mode='constant')
        pimg_shape = image_pad.shape
        h_reduce, w_reduce = (pimg_shape[0] - orig_shape[0]), (pimg_shape[1] - orig_shape[1])

        flat_submatrices = np.array([
            image_pad[i:(i + dilation_level), j:(j + dilation_level)]
            for i in range(pimg_shape[0] - h_reduce) for j in range(pimg_shape[1] - w_reduce)
        ])

        image_dilate = np.array([255 if (i == structuring_kernel).any() else 0 for i in flat_submatrices])
        image_dilate = image_dilate.reshape(orig_shape)
        image_sd.update(image_dilate, '_dilate_')

    @staticmethod
    def erode(image_sd: SPDImage, erosion_level=3):
        erosion_level = 3 if erosion_level < 3 else erosion_level

        structuring_kernel = np.full(shape=(erosion_level, erosion_level), fill_value=255)
        image_src = image_sd.modified_image

        orig_shape = image_src.shape
        pad_width = erosion_level - 2

        image_pad = np.pad(array=image_src, pad_width=pad_width, mode='constant')
        pimg_shape = image_pad.shape
        h_reduce, w_reduce = (pimg_shape[0] - orig_shape[0]), (pimg_shape[1] - orig_shape[1])

        flat_submatrices = np.array([
            image_pad[i:(i + erosion_level), j:(j + erosion_level)]
            for i in range(pimg_shape[0] - h_reduce) for j in range(pimg_shape[1] - w_reduce)
        ])

        image_erode = np.array([255 if (i == structuring_kernel).all() else 0 for i in flat_submatrices])
        image_erode = image_erode.reshape(orig_shape)
        image_sd.update(image_erode, '_erode_')

    @staticmethod
    def erode_raw(image_sd, erosion_level=3):
        erosion_level = 3 if erosion_level < 3 else erosion_level

        structuring_kernel = np.full(shape=(erosion_level, erosion_level), fill_value=255)
        image_src = image_sd

        orig_shape = image_src.shape
        pad_width = erosion_level - 2

        image_pad = np.pad(array=image_src, pad_width=pad_width, mode='constant')
        pimg_shape = image_pad.shape
        h_reduce, w_reduce = (pimg_shape[0] - orig_shape[0]), (pimg_shape[1] - orig_shape[1])

        flat_submatrices = np.array([
            image_pad[i:(i + erosion_level), j:(j + erosion_level)]
            for i in range(pimg_shape[0] - h_reduce) for j in range(pimg_shape[1] - w_reduce)
        ])

        image_erode = np.array([255 if (i == structuring_kernel).all() else 0 for i in flat_submatrices])
        image_erode = image_erode.reshape(orig_shape)
        return image_erode

    @staticmethod
    def otsu(image):
        gray = image
        pixel_number = gray.shape[0] * gray.shape[1]
        mean_weigth = 1.0 / pixel_number
        his, bins = np.histogram(gray, np.array(range(0, 256)))
        final_thresh = -1
        final_value = -1
        for t in bins[1:-1]:  # This goes from 1 to 254 uint8 range (Pretty sure wont be those values)
            Wb = np.sum(his[:t]) * mean_weigth
            Wf = np.sum(his[t:]) * mean_weigth

            mub = np.mean(his[:t])
            muf = np.mean(his[t:])

            value = Wb * Wf * (mub - muf) ** 2

            if value > final_value:
                final_thresh = t
                final_value = value

        final_img = gray.copy()
        final_img[gray > final_thresh] = 255
        final_img[gray < final_thresh] = 0

        final_img[final_img > 230] = 255
        final_img[final_img != 255] = 0

        return  final_img

    @staticmethod
    def automatic_brightness_and_contrast(image: SPDImage, clip_hist_percent=1):
        hist = HistogramModelDriver.histogram(image.modified_image, image.max_type_colors_count())[0]
        accumulator = HistogramModelDriver.cdf(hist)[0]
        maximum = accumulator[-1]
        clip_hist_percent *= (maximum / 100.0)

        minimum_gray = 0
        while accumulator[minimum_gray] < clip_hist_percent:
            minimum_gray += 1

        maximum_gray = len(hist) - 1
        while accumulator[maximum_gray] >= (maximum - clip_hist_percent):
            maximum_gray -= 1

        median = np.median(accumulator)
        medium_gray = 0
        while accumulator[medium_gray] < median:
            medium_gray += 1

        gamma = np.log(np.abs(medium_gray - minimum_gray))
        print(gamma)
        ImageModelDriver.gamma_correction(image, 1, gamma)


"""
hill_cipher.py

@author Elliot and Erica
"""

import random
import numpy as np
from cryptography_utilities import (gcd, extended_gcd)

ALPHABET = 'abcdefghijklmnopqrstuvwxyz'
ALPHABET_SIZE = len(ALPHABET)
N = 3
TOTIENT_26 = 12

def random_matrix(size=N):
    """Generate a NxN matrix filled with random integers"""
    array = [[random.randint(0, ALPHABET_SIZE - 1)
              for _ in range(size)]
             for _ in range(size)]
    return np.matrix(array)

def valid_key(matrix):
    """Determine if the given matrix can be used as a key"""
    return gcd(int(np.linalg.det(matrix)), ALPHABET_SIZE) == 1

def generate_key(size=N):
    """Return a NxN matrix filled with random integers that qualifies
    as a valid key.
    """
    matrix = random_matrix()
    while not valid_matrix(matrix):
        matrix = random_matrix()
    return matrix

def to_int(char):
    """Using ALPHABET, find the numeric representation of the given
    character.
    """
    char_map = {char: number for number, char in enumerate(ALPHABET)}
    return char_map[char]

def to_char(integer):
    """Using ALPHABET, find the character representation of the given
    integer.
    """
    integer_map = {number: char for number, char in enumerate(ALPHABET)}
    return integer_map[integer]

def number_matrix(string):
    """Convert a string into an integer matrix."""
    row = 0
    array = [[] for _ in range(N)]
    for char in make_correct_length(string):
        array[row].append(to_int(char))
        row = (row + 1) % N
    return np.matrix(array)

def make_correct_length(string):
    """Add x characters if the input string cannot be split into even
    blocks.
    """
    if not len(string) % N == 0:
        string += 'x' * (N - (len(string) % N))
    return string

def modular_inverse(number):
    """Calculate the modular multiplicative inverse of a number."""
    gcd, x, y = extended_gcd(ALPHABET_SIZE, number)
    if y < 0:
        return y + 26
    else:
        return y

def switch_key(key):
    """Change an encryption key into a decryption key or
    vice-versa.
    """
    key_determinant = int(np.linalg.det(key))
    inverse = np.linalg.inv(key)
    adjugate = np.multiply(inverse, key_determinant)
    raw_switch = np.multiply(modular_inverse(key_determinant),
                             adjugate)
    modder = np.vectorize(lambda n: int(n) % 26)
    return modder(raw_switch)

def hill_cipher_encrypt(plain_text, key):
    """Determine cipher text given plain text. The key should be an
    NxN matrix.
    """
    cipher_matrix = np.dot(key, number_matrix(plain_text))
    cipher_text = ''
    for number in np.nditer(cipher_matrix):
        cipher_text += to_char(number % ALPHABET_SIZE)
    return cipher_text

def hill_cipher_decrypt(cipher_text, key, encryption_key=False):
    """Determine plain text given cipher text. The key should be an
    NxN matrix.
    """
    if encryption_key:
        key = switch_key(key)
    plain_matrix = np.dot(key, number_matrix(cipher_text))
    plain_text = ''
    for number in np.nditer(plain_matrix):
        plain_text += to_char(number % ALPHABET_SIZE)
    return plain_text


import numpy as np
import statsmodels.api as sm
from gekko import GEKKO

# Data
x0 = np.array([4,5,2,3,-1,1,6,7])
x1 = np.array([3,2,3,4, 3,5,2,6])
y = np.array([0.3,0.8,-0.05,0.1,-0.8,-0.5,0.5,0.65])

# calculate R^2
def rsq(y1,y2):
    yresid= y1 - y2
    SSresid = np.sum(yresid**2)
    SStotal = len(y1) * np.var(y1)
    r2 = 1 - SSresid/SStotal
    return r2

# Method 1: numpy linalg solution
#       Y =     X a
#   X^T Y = X^T X a
X = np.vstack((x0,x1,np.ones(len(x0)))).T
a = np.linalg.lstsq(X,y)[0]; print(a)
yfit = a[0]*x0+a[1]*x1+a[2]
print('R^2 = '+str(rsq(y,yfit)))

# Method 2: statsmodels ordinary least squares
model = sm.OLS(y,X).fit()
predictions = model.predict(X)
print(model.summary())

# Method 3: gekko
m = GEKKO(remote=False); m.options.IMODE=2
c  = m.Array(m.FV,3)
for ci in c:
    ci.STATUS=1
xd = m.Array(m.Param,2); xd[0].value=x0; xd[1].value=x1
yd = m.Param(y); yp = m.Var()
s =  m.sum([c[i]*xd[i] for i in range(2)])
m.Equation(yp==s+c[-1])
m.Minimize((yd-yp)**2)
m.solve(disp=False)
a = [c[i].value[0] for i in range(3)]
print(a)

# plot data
from mpl_toolkits import mplot3d
from matplotlib import cm
import matplotlib.pyplot as plt
fig = plt.figure()
ax  = plt.axes(projection='3d')
ax.plot3D(x0,x1,y,'ko')
x0t = np.arange(-1,7,0.25)
x1t = np.arange(2,6,0.25)
X0,X1 = np.meshgrid(x0t,x1t)
Yt = a[0]*X0+a[1]*X1+a[2]
ax.plot_surface(X0,X1,Yt,cmap=cm.coolwarm,alpha=0.5)
plt.show()


import numpy as np
import matplotlib.pyplot as plt

'''
1)Get the results of a Least Square Linear Regression to model a binary
operator using the normal equations.

2) Check whether the operator can actually be modelled with a linear neural
network
'''

X1 = np.zeros( (4,1) )
X1[0,0] = 0
X1[1,0] = 0
X1[2,0] = 1
X1[3,0] = 1

X2 = np.zeros( (4,1) )
X2[0,0] = 0
X2[1,0] = 1
X2[2,0] = 0
X2[3,0] = 1

y = np.zeros( (4,1) )
y[0,0] = 0
y[1,0] = 1
y[2,0] = 1
y[3,0] = 1

X = np.concatenate( (X1,X2,), axis=1)
X = np.concatenate(  (np.ones((X.shape[0],1) ),X) ,axis=1)

wLS = np.linalg.solve(np.dot(X.T,X),np.dot(X.T,y))
print('Using LS normal equations we obtain wLS = \n',wLS)

fig = plt.figure()
for i in range(0,len(y)):

    if y[i]:
        plt.plot(X1[i],X2[i],marker='o',color="r")
    else:
        plt.plot(X1[i],X2[i],marker='o',color="b")

simArtist = plt.Line2D((0,1),(0,0), color='b', marker='o', linestyle='')
anyArtist = plt.Line2D((0,1),(0,0), color='r',marker='o', linestyle='')
plt.legend([simArtist,anyArtist],['y = 0', 'y = 1'])

plt.gca().set_aspect('equal', adjustable='box')
plt.tight_layout()
plt.xlabel('x1')
plt.ylabel('x2')


# -*- coding: utf-8 -*-
"""
Created on Tue Apr 28 13:11:10 2020

@author: burak
"""
import numpy as np
from matplotlib import pyplot as plt
import math

numSamples = 100000
numBins = 100

# For the random variable Y, Fy(y) = y**2. We have to took derivation of this because we need pdf
def Fy(y,r):
    if y < 0:
        return 0
    elif y <= 1:
        return (2*y)/(r**2)
    else:
        return 0

Fy_array = []
ind = []
c = 1
for i in range(-200,1200):
    Fy_array.append(Fy((i/100),c))
    ind.append(i/100)
    c+=1
plt.figure()
plt.title('Pdf of Y')
plt.plot(ind,Fy_array)
plt.show()
#rejection method
print('----------------------------- Sample rejection method -----------------------------')
c = 2
a = -1
b = 1
Y = []
i = 0
while i < numSamples:
    u = np.random.rand()
    v = np.random.rand()
    y = (b-a)*u+a
    x = c*v
    if x <= Fy(y):
        Y.append(y)
        i = i + 1

plt.figure()
plt.title('Histogram of the generated Y samples (pdf)')
hY = plt.hist(Y, numBins, density=True)
plt.figure()
plt.title('Normalized cumulative sum of histogram values for the generated Y samples (cdf)')
plt.plot(hY[1][0:numBins], np.cumsum(hY[0])/hY[0].sum())
plt.show()

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

# For the random variable X, fx(x) is as the following: we have to took integral of that because of cdf
def fx(x):
    if x < 0:
        fx = 0
    elif x <= 10:
        fx = (math.log(3*x+2)*1.08)/3
    else:
        fx = 1
    return fx

fx_array = []
ind = []
for i in range(-100,200):
    fx_array.append(fx(i/100))
    ind.append(i/100)
plt.figure()
plt.title('Cdf of X')
plt.plot(ind,fx_array)
plt.show()
#inverse transformation method
print('-------------------------- Inverse transformation method --------------------------')
def fx_inverse(u):
    return (math.e**(3*u/1.08)-2)/3
U = []
X = []
for i in range(numSamples):
    U.append(np.random.rand())
    X.append(fx_inverse(U[i]))
plt.figure()
plt.title('Histograms of the generated U and X samples (pdf)')
hU = plt.hist(U,numBins,alpha=0.5, density=True)
hX = plt.hist(X,numBins,alpha=0.5, density=True)
plt.figure()
plt.title('Normalized cumulative sum of histogram values for the generated U and X samples (cdf)')
plt.plot(np.cumsum(hU[0])/hU[0].sum())
plt.plot(np.cumsum(hX[0])/hX[0].sum())
plt.show()


#!/usr/bin/env python3
# coding=utf-8

import numpy as np
from sklearn import datasets
import matplotlib.pyplot as plt
import kaiLogistic.logistic_from_mock_data_utils as kai

random_state = np.random.RandomState(2)
data, target = datasets.make_blobs(n_samples=200, n_features=2, centers=2, cluster_std=1.0, random_state=random_state)
# print('data=%s' % data)
# print('target=%s' % target)

var_x1 = [x[0] for i, x in enumerate(data)]
var_x2 = [x[1] for i, x in enumerate(data)]

def sigmoid(z):
    exp = np_exp(-z)
    return 1.0 / (1 + exp)

def np_exp(array):
    return np.exp(np.minimum(array, 700))

def np_log(array):
    return np.log(np.maximum(array, 1e-250))

loss_type = 3

# 逻辑回归 损失函数 凸函数 证明
# 证明LogLoss是凸函数
# http://sofasofa.io/forum_main_post.php?postid=1000921
# http://lotabout.me/2018/Logistic-Regression-Notes/
def calc_loss(_b=0, _w1=0, _w2=0):
    result_mul1 = np.multiply(var_x1, _w1)
    result_mul2 = np.multiply(var_x2, _w2)
    result_add = result_mul1 + result_mul2 + _b
    result_sigmoid = sigmoid(result_add)

    if loss_type == 1 or loss_type == 2:
        if loss_type == 1:
            loss_array = np.square(result_add - target)  # linear regression square loss
        else:
            loss_array = np.square(result_sigmoid - target)  # logistic regression square loss
        return np.sqrt(np.mean(loss_array))
    elif loss_type == 3:
        first = np.multiply(-target, np_log(result_sigmoid))  # logistic regression log loss
        second = np.multiply(1 - target, np_log(1 - result_sigmoid))
        loss_array = first - second
        return np.average(loss_array)
    else:
        loss_array = np.maximum(result_add, 0) - result_add * target + np.log(1 + np.exp(-np.abs(result_add)))
        return np.average(loss_array)

def predict(_x1, _x2, _b, _w1, _w2):
    result = _b + _x1 * _w1 + _x2 * _w2
    result_sigmoid = 1 / (1 + np_exp(-result))
    return result_sigmoid

if __name__ == '__main__':
    loss_vec = []

    max_n = 200
    b = -10
    w2 = 20
    test_range = np.arange(-max_n, max_n, 1)
    for step in test_range:
        loss = calc_loss(b, step, w2)
        loss_vec.append(loss)
        # print('step=%d loss=%s' % (step, loss))

    plt.figure()
    plt.title('loss type: ' + str(loss_type))
    plt.plot(test_range, loss_vec, 'g-')
    plt.show()

    show_visual = 0
    if show_visual:
        class1_x = [x[0] for i, x in enumerate(data) if target[i] == 1]
        class1_y = [x[1] for i, x in enumerate(data) if target[i] == 1]
        class2_x = [x[0] for i, x in enumerate(data) if target[i] != 1]
        class2_y = [x[1] for i, x in enumerate(data) if target[i] != 1]

        w1 = 200
        # w1 = 50
        frame, _ = kai.make_visualization_frame(class1_x, class1_y, class2_x, class2_y)
        series_x1 = frame['x1']
        series_x2 = frame['x2']
        frame['probabilities'] = predict(series_x1, series_x2, b, w1, w2)

        kai.show_visualization_data(class1_x, class1_y, class2_x, class2_y
                                    , None
                                    , None, None
                                    , 'Test Loss'
                                    , frame)


# -*- coding: utf-8 -*-
"""
Created on Sun Nov  7 01:52:10 2021

@author: Aerial
"""
# Import lib
import numpy as np
import sympy as sp
from sympy import *
import matplotlib.pyplot as plt

# u = U_nod * (1+x/L)
# v = -1 * U_nod * (y/L)
# Solve for dydx = v/u
x, y, U_nod, L = sp.symbols("x, y, U_nod, L")
# Left hand side of the eq
lhs = 1 / (U_nod * (1 + x/L))
# Integrate left hand side of the eq
lhs_int = sp.integrate(lhs,x)
sp.simplify(lhs_int)
print("Left handside of ODE equals = {0}".format(lhs_int))
# Right hand side of the eq
rhs = 1 / (U_nod * (y / L))
# Integrate right hand side of the eq
rhs_int = sp.integrate(rhs,y)
sp.simplify(rhs_int)
print("Right handside of ODE equals = {0}".format(rhs_int))
# Set rhs_int = lhs_int ( seperable ODE )
# After solving for y, we plot for 4 different values of constant

# Define variables
C1 = 1.0
C2 = 0.75
C3 = 0.50
C4 = 0.25
L = 1.0
# Set L = 1 so we can plot for x/L and y/L
x = np.linspace(0,2,150)
y1 = L*C1 / (1 + x / L)
y2 = L*C2 / (1 + x / L)
y3 = L*C3 / (1 + x / L)
y4 = L*C4 / (1 + x / L)

# Plot
plt.figure()
plt.xlabel("[ x/L ]")
plt.ylabel("[ y/L ]")
plt.plot(x,y1 ,color='red', label = "C = 1.00")
plt.plot(x,y2 ,color='grey', label = "C = 0.75")
plt.plot(x,y3 ,color='black', label = "C = 0.50")
plt.plot(x,y4 ,color='blue', label = "C = 0.25")
plt.legend()
plt.savefig("ch4_4.pdf", dpi = 300)


import numpy as np

def sigmoid(Z):
    """ Calculates the sigmoid activation function on Z

    Preconditions:
    Z: non-empty numpy ndarray containing linear transformations of the previous layer's activations or input layer.

    Parameters:
    Z: The numpy array that will be used to calculate the activations of a layer

    Postconditions:
    A: non-empty numpy ndarray that contains the non-linear transformations of Z
    """

    A = 1 / (1 + np.exp(-Z))

    return A

def ReLu(Z):
    """ Calculates the Rectified linear unit (ReLu) activation function on Z

    Preconditions:
    Z: non-empty numpy ndarray containing linear transformations of the previous layer's activations or input layer.

    Parameters:
    Z: The numpy array that will be used to calculate the activations of a layer

    Postconditions:
    A: non-empty numpy ndarray that contains the non-linear transformations of Z
    """
    # With relu if the value of Z < 0 it is 0 otherwise it is passed through a
    # linear function of slope 1 which is why its derivative is either 0 or 1.
    A = np.maximum(0, Z)

    return A

def ReLu_derivative(dA, Z):
    """ Calculates/Produces the derivative of the cost function with respect to Z when Z was passed through.

    Preconditions:
    dA: non-empty numpy narray with the same dimensions as Z
    Z: non-empty numpy ndarray containing linear transformations of the previous layers activations or input layer.

    Parameters:
    dA: Numpy array containing the derivative of the cost function with respect to the activations (dC/dA)
    Z: The numpy array that will be used to calculate the activations of a layer

    Postconditions:
    dZ: non-empty numpy ndarray that contains the derivative of the cost function with respect to Z (dC/dZ)
    """

    # We can make a copy of dA because wherever Z > 0 dA/dZ = 1
    # and since dC/dZ = (dC/dA)x(dA/dZ) we can just initialize it to be dA and change it when Z <= 0
    dZ = np.array(dA, copy=True)

    # where ever Z <= 0 the derivative is changed to 0
    dZ[Z <= 0] = 0

    return dZ

def sigmoid_derivative(dA, Z):
    """ Calculates/Produces the derivative of the cost function with respect to the Z when Z was passed through the sigmoid.

    Preconditions:
    dA: non-empty numpy narray with the same dimensions as Z
    Z: non-empty numpy ndarray containing linear transformations of the previous layers activations or input layer.

    Parameters:
    dA: Numpy array containing the derivatives of the cost function with respect to the activations (dC/dA)
    Z: The numpy array that will be used to calculate the activations of a layer

    Postconditions:
    dZ: non-empty numpy ndarray that contains the derivatives of the cost function with respect to Z (dC/dZ)
    """

    s = 1 / (1 + np.exp(-Z))

    # dC represents the derivative of the cost function
    # multiplies dC/dA by the dA/dZ to obtain the dC/dZ
    dZ = dA * s * (1 - s)

    return dZ


'''
Reference sources:
Recktenwald, G. W. (2004). Finite-difference approximations to the heat equation.
Mechanical Engineering, 10(01).
'''
from math import *
import matplotlib.pyplot as plt
import numpy as np
plt.style.use('dark_background')
import matplotlib
matplotlib.rcParams['mathtext.fontset'] = 'stix'
matplotlib.rcParams['font.family'] = 'STIXGeneral'

class HeatEquationSolver:

  def __init__(self, Nx=60, x_max=50, Mt=100, t_max=10,
               f_0=lambda x: sin(x) + x**(1/2),
               alpha=1, heat_x_0=None, heat_x_max=None,
               save_plots=False):
    self.N = Nx
    self.x_max = x_max
    self.dx = self.x_max/(self.N-1)
    self.x_i = lambda i: self.dx*i

    self.M = Mt
    self.t_max = t_max
    self.dt = self.t_max/(self.M-1)
    self.t_m = lambda m: self.dt*m

    # Initial condition (t=0)
    self.f_0 = f_0

    self.alpha = alpha

    self.heat_x_0 = heat_x_0
    self.heat_x_max = heat_x_max
    if self.heat_x_0 == None: self.heat_x_0 = self.f_0(0)
    if self.heat_x_max == None: self.heat_x_max = self.f_0(self.x_max)

    self.save_plots = save_plots

  def condition(self, x_i, t_m):
    # "Constant" boundary conditions
    if x_i == 0:
      return self.heat_x_0

    if x_i == self.x_max:
      return self.heat_x_max

    # Enforce condition (t=0)
    if t_m == 0:
      return self.f_0(x_i)

    return 0

  def get_solution(self):
    u = [[self.condition(self.x_i(i), self.t_m(m)) for i in range(self.N)]
         for m in range(self.M)]

    # Computational improvement, equation 17 in Recktenwald (2004)
    r = (self.alpha*self.dt)/(self.dx**2)
    r2 = 1 - 2*r

    # Forward Time, Centered Space (FTCS) Solution
    for m in range(1,self.M): # Exclude initial condition
      for i in range(1, self.N-1): # Exclude boundaries
        u[m][i] = r*u[m-1][i-1] + r2*u[m-1][i] + r*u[m-1][i+1]

    return u

  def plot(self, heat_equation_solution, time_m, show=True):
    x_values = [self.x_i(i) for i in range(self.N)]
    u_values = heat_equation_solution[time_m]
    plt.plot(x_values, u_values,
             label=f"$t$ = {round(self.t_m(time_m),2)}")
    plt.legend(fontsize='large')
    plt.xlabel("$x$", fontsize=20)
    plt.ylabel("Heat", fontsize=20)
    if show: self.show()

  def show(self):
    if self.save_plots:
      print("Save fig")
      plt.savefig('/tmp/heat_equation.png', bbox_inches='tight',
                  dpi=300)
      print("Fig saved successfully.")
    else:
      plt.show()

  def plot_stacked(self, u_solution):
    plt.figure(figsize=(8,4))
    m_times = np.linspace(0,self.M-1,5).astype(int)
    # print(m_times)
    for m in np.linspace(0,self.M-1,5).astype(int):
      is_last = m == self.M-1
      self.plot(u_solution, time_m=m, show=is_last)

def test_1():
    solver = HeatEquationSolver(save_plots=True)

    u_solution = solver.get_solution()

    # solver.plot(u_solution, time_m=10)
    solver.plot_stacked(u_solution)

def test_2():
    L = 1
    f_0 = lambda x: sin(pi*x/L)
    phi_0 = phi_L = 0
    alpha=0.1
    solver = HeatEquationSolver(f_0=f_0,
                                Nx=20, x_max=1,
                                heat_x_0=phi_0,
                                heat_x_max=phi_L,
                                Mt=20, t_max=1,
                                alpha=0.1,
                                save_plots=True)

    u_solution = solver.get_solution()

    solver.plot_stacked(u_solution)

if __name__ == '__main__':
    test_2()


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

# [0,0] = TN
# [1,1] = TP
# [0,1] = FP
# [1,0] = FN

# cm is a confusion matrix

# Accuracy: (TP + TN) / Total
def accuracy(cm: pd.DataFrame) -> float:
    return (cm[0,0] + cm[1,1]) / cm.sum()

# Precision: TP / (TP + FP)
def precision(cm: pd.DataFrame) -> float:
    return cm[1,1] / (cm[1,1] + cm[0,1])

# False positive rate: FP / N = FP / (FP + TN)
def false_positive(cm: pd.DataFrame) -> float:
    return cm[0,1] / (cm[0,0] + cm[0,1])

# True positive rate: TP / P = TP / (TP + FN)
# Equivalent to sensitivity/recall
def true_positive(cm: pd.DataFrame) -> float:
    return cm[1,1] / (cm[1,0] + cm[1,1])

# F1 score:  2 * precision * recall / (precision + recall)
def f_score(cm: pd.DataFrame) -> float:
    return 2 * precision(cm) * true_positive(cm) / (precision(cm) + true_positive(cm))

# Returns a confusion matrix for labels and predictions
# [[TN, FP],
#  [FN, TP]]
def confusion_matrix(y, y_hat):
    cm = np.zeros((2, 2))
    np.add.at(cm, [y.astype(int), y_hat.astype(int)], 1)
    return cm

def visualize_cm(cm):
    df_cm = pd.DataFrame(cm, columns=['0', '1'], index=['0', '1'])
    df_cm.index.name = 'Actual'
    df_cm.columns.name = 'Predicted'
    plt.figure(figsize=(5, 3))
    sns.heatmap(df_cm, cmap='Blues', annot=True, annot_kws={'size': 16}, fmt='g')

# Function to return two shuffled arrays, is a deep copy
def shuffle(x, y):
    x_copy = x.copy()
    y_copy = y.copy()
    rand = np.random.randint(0, 10000)
    np.random.seed(rand)
    np.random.shuffle(x_copy)
    np.random.seed(rand)
    np.random.shuffle(y_copy)
    return x_copy, y_copy

# Shuffles and splits data into two sets
# test split will be 1/size of the data
def split(x, y, size):
    x1, y1, = shuffle(x, y)
    x1_test = x1[0:int(x1.shape[0] / size)]
    x1_train = x1[int(x1.shape[0] / size):]
    y1_test = y1[0:int(y1.shape[0] / size)]
    y1_train = y1[int(y1.shape[0] / size):]
    return x1_train, x1_test, y1_train, y1_test

def cross_validation(k, X, Y, model, lr=0.5, regularization=0, eps=1e-2, verbose=True):
    # randomize X and Y by shuffling
    x, y = shuffle(X, Y)

    # split into k folds
    x_folds = np.array_split(x, k)
    y_folds = np.array_split(y, k)

    acc = 0
    f1 = 0
    prec = 0
    rec = 0
    cms = []

    for i in range(k):
        validation_features = x_folds[i]
        validation_labels = np.squeeze(y_folds[i])

        train_features = np.delete(x_folds, i, axis=0)
        train_features = np.concatenate(train_features)
        train_labels = np.delete(y_folds, i, axis=0)
        train_labels = np.concatenate(train_labels)

        m = model(train_features, train_labels)
        m.fit(lr, verbose=False, regularization=regularization, eps=eps)

        predicted_labels = m.predict(validation_features)
        cm = confusion_matrix(validation_labels, predicted_labels)

        acc += accuracy(cm)
        f1 += f_score(cm)
        prec += precision(cm)
        rec += true_positive(cm)
        cms.append(cm)

    if verbose:
        print("Accuracy:", acc/k, "Precision:", prec/k, "Recall:", rec/k, "F1:", f1/k)
    # Return the accuracy and array of confusion matrices
    return acc/k, np.array(cms)

# assume 5 fold for now
def cross_validation_naive(k, df, model, label, cont=[], cat=[], bin=[]):
    df = df.copy(deep=True)
    np.random.shuffle(df.values)
    df = df.reset_index(drop=True)

    indices = np.arange(df.shape[0])
    indices = np.array_split(indices, k)

    acc = 0
    f1 = 0
    prec = 0
    rec = 0
    cms = []

    for i in range(k):
        val = df.loc[indices[i]]

        train = df.loc[np.concatenate(np.delete(indices, i, axis=0))]

        m = model(train, label, cont, cat, bin)
        pred = val.apply(m.predict, axis=1)

        cm = confusion_matrix(val[label], pred)

        acc += accuracy(cm)
        f1 += f_score(cm)
        prec += precision(cm)
        rec += true_positive(cm)
        cms.append(cm)

    print("Accuracy:", acc / k, "Precision:", prec / k, "Recall:", rec / k, "F1:", f1 / k)

        # Return the accuracy and array of confusion matrices
    return acc / k, np.array(cms)

def cv_task_2(k, X, Y, model, lr = 0.5, regularization=0, eps = 1e-2, iterations=200):
    # randomize X and Y by shuffling
    x, y = shuffle(X, Y)

    # split into k folds
    x_folds = np.array_split(x, k)
    y_folds = np.array_split(y, k)

    train_acc_history = np.empty([k, iterations])
    val_acc_history = np.empty([k, iterations])

    for i in range(k):
        val_features = x_folds[i]
        val_labels = np.squeeze(y_folds[i])

        train_features = np.delete(x_folds, i)
        train_features = np.concatenate(train_features)
        train_labels = np.delete(y_folds, i, axis=0)
        train_labels = np.concatenate(train_labels)

        m = model(train_features, train_labels)

        costs = []
        train_accuracies = []
        val_accuracies = []

        # Keep on training until difference reached threshold
        for j in range(iterations):

            # fit model for 1 iteration
            cost = m.fit(lr=lr, verbose=False, regularization=regularization, eps=None, epochs=1)
            costs.append(cost)

            # predict the labels and eval accuracy for train and val split
            val_pred_labels = m.predict(val_features)
            train_pred_labels = m.predict(train_features)
            cm_val = confusion_matrix(val_labels, val_pred_labels)
            cm_train = confusion_matrix(train_labels, train_pred_labels)

            val_accuracies.append(accuracy(cm_val))
            train_accuracies.append(accuracy(cm_train))

        # store the costs and accuracies
        train_acc_history[i] = np.array(train_accuracies)
        val_acc_history[i] = np.array(val_accuracies)

    return train_acc_history, val_acc_history

def grid_search(learning_rates, epsilons, lambdas, x, y, model):
    max_acc = 0
    arg_max = [0,0,0]

    for lr in learning_rates:
        for eps in epsilons:
            for regularization in lambdas:
                #print(lr, eps, regularization)
                acc, cm = cross_validation(5, x, y, lr=lr, eps=eps, regularization=regularization, model=model, verbose=False)
                if acc > max_acc:
                    max_acc = acc
                    arg_max = [lr, eps, regularization]
                    max_cm = cm
    f1 = []
    prec = []
    rec = []
    for cm in max_cm:
        f1.append(f_score(cm))
        prec.append(precision(cm))
        rec.append(true_positive(cm))

    f1 = np.mean(f1)
    prec = np.mean(prec)
    rec = np.mean(rec)

    print(arg_max)
    print("Accuracy:", max_acc, "Precision:", prec, "Recall:", rec, "F1:", f1)

    return max_acc, arg_max


import numpy as np

# This script generates S-Box and its inverse
# This script is written based on my understanding about the
# algorithm that generates the S-Box

# Rotate a byte with a specified amount
def ROTL8(x, shift):
	return (x << shift) | (x >> (8 - shift))

# Multiplication in finite field function
def gmul(a, b):
    i = 0
    product = 0
    for i in range(8):
        if ((b & 1) == 1):
            product ^= a
        if ((a & 0x80) == 0x80):
            a = a << 1
            a = a ^ 0x11b
        else:
            a = a << 1
        b = b >> 1
    return int(product)

sbox = np.zeros(256, np.uint8)
i_sbox = np.zeros(256, np.uint8)
count, p, q = 0, 1, 1

for i in range(256):
	# Multiply p by 3
	p = gmul(p, 0x03)

	# Divide q by 3 (equals multiplication by 0xF6)
	q = gmul(q, 0xF6)

	# Compute the affine transformation
	xformed = q ^ ROTL8(q, 1) ^ ROTL8(q, 2) ^ ROTL8(q, 3) ^ ROTL8(q, 4)

	sbox[p] = xformed ^ 0x63
	i_sbox[sbox[p]] = p

# 0 is a special case since it has no inverse
sbox[0] = 0x63

# Print S-Box
for i in range(256):
	if (count < 16):
		count += 1
	else:
		count = 1
		print()
	out = hex(sbox[i]).replace("0x", "").zfill(2)
	print(out, " ", sep="", end="")

print()

# Print Inverse S-Box
for i in range(256):
	if (count < 16):
		count += 1
	else:
		count = 1
		print()
	out = hex(i_sbox[i]).replace("0x", "").zfill(2)
	print(out, " ", sep="", end="")


# Undergraduate Student: Arturo Burgos
# Professor: João Rodrigo Andrade
# Federal University of Uberlândia - UFU, Fluid Mechanics Laboratory - MFLab, Block 5P, Uberlândia, MG, Brazil

# Fourth exercise: Solving a Linear System --> ax = b

# Here I first set conditions

import numpy as np
from numpy import linalg as lin
np.seterr(divide='ignore', invalid='ignore')

print('\n')

# Basic definitions
initialLineValue = 0
initialColumnValue = 0

pointsAmount = 9
pointsRoot = int(np.sqrt(pointsAmount))
#K = np.sqrt(pointsAmount)
#k = int(K)

# Initial x_k = old and x_k1 = current value
old = 0
current = 1

valuesVector = np.zeros((2,pointsAmount))

valuesVector[old] = np.zeros(pointsAmount)
valuesVector[current] = np.ones(pointsAmount)

# Here I set the Matrix

indexMatrix = np.zeros((pointsAmount, pointsAmount))

for matrixRow in range(initialLineValue, pointsAmount):
    for matrixCol in range(initialLineValue, pointsAmount):
        if matrixRow == matrixCol:
            indexMatrix[matrixRow, matrixCol] = -4
        elif matrixRow == matrixCol - 3 or matrixRow ==matrixCol + 3:
            indexMatrix[matrixRow, matrixCol] = 1
        elif ((matrixRow + 1) % 3 != 0 and matrixRow == matrixCol - 1) or ((matrixRow + 1) % 3 != 1 and matrixRow == matrixCol + 1): # (i+1) because in Python we start from 0
            indexMatrix[matrixRow, matrixCol] = 1

print('The coefficient Matrix is:')
print(indexMatrix)
print('\n')

resultMatrix = np.zeros(pointsAmount)

for matrixRow in range(initialLineValue, pointsRoot):
    if matrixRow < pointsRoot - 1: # (k-1) because in Python we start from 0
        resultMatrix[matrixRow] = -50
    else:
        resultMatrix[matrixRow] = -150

for matrixRow in range(pointsRoot, pointsAmount - pointsRoot):
    if (matrixRow + 1) % 6 != 0: # (i+1) because in Python we start from 0
        resultMatrix[matrixRow] = 0
    else:
        resultMatrix[matrixRow] = -100

for matrixRow in range(pointsAmount - pointsRoot, pointsAmount):
    if matrixRow < pointsAmount - 1: # (k-1) because in Python we start from 0
        resultMatrix[matrixRow] = -50
    else:
        resultMatrix[matrixRow] = -150

print('The result Matrix is:')
print(resultMatrix)
print('\n')

# Here I set the tolerance
tolerance = 0.0000001
# Here I set the iterations
iterationsCounter = 0

# Here I set the error based in the Infinite norm
error = (valuesVector[current] - valuesVector[old]) / valuesVector[current]

while error.all() > tolerance:
    for matrixRow in range(initialLineValue, pointsAmount):

        #x_k1[i] = b[i].copy()
        valuesVector[current][matrixRow] = resultMatrix[matrixRow]

        for matrixCol in range(initialColumnValue, pointsAmount):
            if(matrixCol != matrixRow):
                valuesVector[current][matrixRow] = valuesVector[current][matrixRow] - indexMatrix[matrixRow, matrixCol] * valuesVector[old][matrixCol]
        #x_k1[i] =  x_k1[i].copy() / a[i,i].copy()
        valuesVector[current][matrixRow] = valuesVector[current][matrixRow] / indexMatrix[matrixRow, matrixRow]

    error = (valuesVector[current] - valuesVector[old]) / valuesVector[current]
    valuesVector[old] = valuesVector[current]
    iterationsCounter = iterationsCounter + 1

print('The number of iterations is: ')
print(iterationsCounter)

print('The solution is:')
print(valuesVector[current])


# Problemas (Física)

## Problema (Movimento de partícula)

A posição de uma partícula à medida que se move ao longo do eixo $y$ é dada por
$y(t) = 2\sin\left(\frac{\pi t}{4}\right),$
com $t$ em segundos e $y$ em centímetros. Calcule:

a. A velocidade média da partícula entre $t = 0$ e $t=2.0 \, s$.

b. A velocidade instantânea da partícula em $t = 0, \, 1.0$ e $2.0 \, s$.

c. A aceleração média da partícula entre $t = 0$ e $t=2.0 \, s$.

d. A aceleração instantânea da partícula em $t = 0, \, 1.0$ e $2.0 \, s$.

(Adaptado de Halliday & Resnick)

### Resolução

import numpy as np
import sympy as sym
import matplotlib.pyplot as plt

def y(t):
    return 2*np.sin( (np.pi*t)/4 )

# calculo de média
def m(tf,t0,f):
    return (f(tf) - f(t0))/(tf - t0)

# a.
print(f'Velocidade média = {np.round(m(0,2,y),2)} cm/s')

# b. c. e d.

# Cria expressão simbólica
tsym = sym.Symbol('t')
def ysym(tsym):
    return 2*sym.sin( (sym.pi*tsym)/4 )
print(f'Velocidade média = {np.round(m(0,2,y),2)} cm/s')

# Calcula velocidade instantânea
for t0 in [0.,1.0,2.0]:
    dydt = sym.diff(ysym(tsym),tsym) # dy/dt
    vinst = dydt.subs({'t':t0})
    print(f'Velocidade instantânea em t = {t0} é {vinst.evalf(3)} cm/s')

for t0 in [0.,1.0,2.0]:
    if t0 == 2.0:
        amed = (dydt.subs({'t':t0}) - dydt.subs({'t':0}))/2.0
        print(f'Aceleração média = {amed.evalf(2)} cm/s^2')

    d2ydt2 = sym.diff(ysym(tsym),tsym,2) # d2y/dt2
    ainst = d2ydt2.subs({'t':t0})
    print(f'Aceleração instantânea em t = {t0} é {ainst.evalf(3)} cm/s^2')

tnum = np.linspace(0,5)
plt.plot(tnum,y(tnum));


#!/usr/bin/env python

# The task is to plot relative error in differentiaion
# versus the interval size used
# on a log-log plot

import numpy as np
import matplotlib.pyplot as plt
#libraries

#specify number of points
N = int(input("How many points? "))

#describe all interval sizes
H = []
h = 0.25*np.pi
for i in range(N):
    H.append(h)
    h = 0.5*h

H = np.array(H)
x_0 = 0.25*np.pi
#generate everything we'll need
sin_i = np.sin(x_0+0.0*H)
sin_i_f1 = np.sin(x_0+1.0*H)
sin_i_f2 = np.sin(x_0+2.0*H)
sin_i_b1 = np.sin(x_0-1.0*H)
sin_i_b2 = np.sin(x_0-2.0*H)

exact_diff = np.cos(x_0)

#Order h, and relative error
diff_Oh = (sin_i_f1-sin_i)/H
rel_Oh = np.abs(diff_Oh-exact_diff)/exact_diff

#Order h^2, and relative error
diff_Oh2 = (sin_i_f1-sin_i_b1)/(2*H)
rel_Oh2 = np.abs(diff_Oh2-exact_diff)/exact_diff

#Order h^4, and relative error
diff_Oh4 = (8*(sin_i_f1-sin_i_b1)-(sin_i_f2-sin_i_b2))/(12*H)
rel_Oh4 = np.abs(diff_Oh4-exact_diff)/exact_diff

#plot now
fig = plt.figure()
fig.suptitle('Error vs Interval Size',fontsize=12,fontweight='bold')
ax = fig.add_subplot(111)
ax.set_xlabel('Log h')
ax.set_ylabel('Log Relative Error')

plt.loglog(H,rel_Oh,label='Order h')
plt.loglog(H,rel_Oh2,label='Order h^2')
plt.loglog(H,rel_Oh4,label='Order h^4')

plt.legend(bbox_to_anchor=(0.,0.),loc=3,borderaxespad=0.)
plt.show()


import numpy as np

# IMPORTANTE!
# si A es estrictamente diagonal dominante, entonces tanto
# Jacobi como Gauss-Seidel convergen si o si.
# Si la Matriz es simétrica y definida positiva G-S converge
# si o si.

def diagonalyDominantCheck(A):
    '''

    :param A: matriz a analizar
    :return: bool, true si A es diagonalmente dominante
    '''
    isDominant = True
    shape = A.shape[0]
    for i in range(shape):

        # Sumo todos los valores absolutos de los elementos
        # de la fila menos el elemento diagonal
        tempSum = 0
        for j in range(shape):
            if j != i:
                tempSum += abs(A[i, j])

        # Me fijo que la fila respete la dominancia diagonal
        if abs(A[i, i]) < tempSum:
            isDominant = False
            break
    return isDominant

def jacobiIteration(A, b):
    '''
    resuelve sistema lineal por iteración de JACOBI.
    criterio de convergencia: A es estrictamente diagonal dominante

    :param A: matriz de coeficientes
    :param b: vector solución (IMPORTANTE: b.shape[1] = 1, es vector columna)
    :return x: vector de incógnitas
    '''

    x = np.full(b.shape, 1)
    nextx = np.zeros(b.shape)
    for _ in range(100):
        for i in range(x.shape[0]):
            tempSum = 0
            for j in range(A.shape[1]):
                if j != i:
                    tempSum += A[i, j] * x[j]

            nextx[i] = (b[i] - tempSum) / A[i, i]

        x = nextx

    return x

def GaussSeidelIteration(A, b):
    '''
    resuelve sistema lineal por iteración de JACOBI.
    criterio de convergencia: A es estrictamente diagonal dominante o
    A es simétrica y definida positiva

    :param A: matriz de coeficientes
    :param b: vector solución (IMPORTANTE: b.shape[1] = 1, es vector columna)
    :return x: vector de incógnitas
    '''

    x = np.full(b.shape, 1)
    nextx = np.zeros(b.shape)
    for _ in range(100):
        for i in range(x.shape[0]):
            tempSum1 = 0
            for j in range(i):
                tempSum1 += A[i, j] * nextx[j]
            tempSum2 = 0
            for j in range(i + 1, x.shape[0]):
                tempSum2 += A[i, j] * x[j]

            nextx[i] = (b[i] - tempSum1 - tempSum2) / A[i, i]
        x = nextx

    return x

print(jacobiIteration(np.array([[2, 1], [1, 3]]), np.array([[5], [6]])))
print(GaussSeidelIteration(np.array([[2, 1], [1, 3]]), np.array([[5], [6]])))


'''
This file contains Python codes for computing the Squared Euclidean Distance
Matrix between points based on the functions proposed by Bauckhage (2014).

Bauckhage, C. (2014). NumPy / SciPy Recipes for Data Science: Squared Euclidean
Distance Matrices, https://dx.doi.org/10.13140/2.1.4426.1127
'''

# import numpy
import numpy as np
# import numpy linear algebra module
import numpy.linalg as la
#import numba
from numba import njit

def naive(P, S):
    '''
    Compute the Squared Euclidean Distance Matrix between the points in P and S.
    This code uses the function numpy.linalg.norm in a "doubly-nested for".

    parameters
    ----------
    P: numpy array 2D
        3 x N matrix containing the coordinates x (1rt row), y (2nd row),
        z (3rd row) of N points. The ith column represents the ith point.
    S: numpy array 2d
        3 x M matrix containing the coordinates x (1rt row), y (2nd row),
        z (3rd row) of M points. The ith column represents the ith point.

    returns
    -------
    D: numpy array 2D
        N x M matrix whose elment ij of the squared Euclidean distance between
        the ith point in P and the jth point in S.
    '''

    # verify input and get number of rows/columns
    Mp, Np, Ms, Ns = _check_input(P, S)

    # initialize squared EDM D
    D = np.zeros((Np,Ns))

    # iterate over all rows of D
    for i in range(Np):
        for j in range(Ns):
            D[i,j] = la.norm(P[:,i] - S[:,j])**2
    return D

def avoid_sqrt(P, S):
    '''
    Compute the Squared Euclidean Distance Matrix between the points in P and S.
    This code avoids computing sqrt by using the function numpy.dot in a
    "doubly-nested for".

    parameters
    ----------
    P: numpy array 2D
        3 x N matrix containing the coordinates x (1rt row), y (2nd row),
        z (3rd row) of N points. The ith column represents the ith point.
    S: numpy array 2D
        3 x M matrix containing the coordinates x (1rt row), y (2nd row),
        z (3rd row) of M points. The ith column represents the ith point.

    returns
    -------
    D: numpy array 2D
        N x M matrix whose element ij of the squared Euclidean distance between
        the ith point in P and the jth point in S.
    '''

    # verify input and get number of rows/columns
    Mp, Np, Ms, Ns = _check_input(P, S)

    # initialize squared EDM D
    D = np.zeros((Np,Ns))

    # iterate over all rows of D
    for i in range(Np):
        for j in range(Ns):
            d = P[:,i] - S[:,j]
            D[i,j] = np.dot(d, d)
    return D

def vectorized(P, S):
    '''
    Compute the Squared Euclidean Distance Matrix between the points in P and S.
    This code avoids computing sqrt and inner loops by using a vectorized
    approach.

    parameters
    ----------
    P: numpy array 2D
        3 x N matrix containing the coordinates x (1rt row), y (2nd row),
        z (3rd row) of N points. The ith column represents the ith point.
    S: numpy array 2D
        3 x M matrix containing the coordinates x (1rt row), y (2nd row),
        z (3rd row) of M points. The ith column represents the ith point.

    returns
    -------
    D: numpy array 2D
        N x M matrix whose element ij of the squared Euclidean distance between
        the ith point in P and the jth point in S.
    '''

    # verify input and get number of rows/columns
    Mp, Np, Ms, Ns = _check_input(P, S)

    # compute components of matrix D
    D1 = np.sum(a=P*P, axis=0)
    D2 = np.sum(a=S*S, axis=0)
    D3 = 2*np.dot(P.T, S)

    # use broadcasting rules to add D1, D2 and D3
    D = D1[:,np.newaxis] + D2[np.newaxis,:] - D3

    return D

@njit
def naive_numba(P, S):
    '''
    Compute the Squared Euclidean Distance Matrix between the points in P and S.
    This code uses numba to optimize the naive approach with the function
    numpy.linalg.norm in a "doubly-nested for".

    parameters
    ----------
    P: numpy array 2D
        3 x N matrix containing the coordinates x (1rt row), y (2nd row),
        z (3rd row) of N points. The ith column represents the ith point.
    S: numpy array 2d
        3 x M matrix containing the coordinates x (1rt row), y (2nd row),
        z (3rd row) of M points. The ith column represents the ith point.

    returns
    -------
    D: numpy array 2D
        N x M matrix whose elment ij of the squared Euclidean distance between
        the ith point in P and the jth point in S.
    '''

    # verify input and get number of rows/columns
    assert P.ndim == S.ndim == 2, 'P and S must be 2D arrays'
    # determine dimensions of data matrices P and S
    Mp,Np = P.shape
    Ms,Ns = S.shape
    assert Mp == Ms == 3, 'P and S must have 3 rows'

    # initialize squared EDM D
    D = np.zeros((Np,Ns))

    # iterate over all rows of D
    for i in range(Np):
        for j in range(Ns):
            D[i,j] = la.norm(P[:,i] - S[:,j])**2
    return D

@njit
def avoid_sqrt_numba(P, S):
    '''
    Compute the Squared Euclidean Distance Matrix between the points in P and S.
    This code uses numba to optimize the approach with the function numpy.dot
    in a "doubly-nested for".

    parameters
    ----------
    P: numpy array 2D
        3 x N matrix containing the coordinates x (1rt row), y (2nd row),
        z (3rd row) of N points. The ith column represents the ith point.
    S: numpy array 2D
        3 x M matrix containing the coordinates x (1rt row), y (2nd row),
        z (3rd row) of M points. The ith column represents the ith point.

    returns
    -------
    D: numpy array 2D
        N x M matrix whose element ij of the squared Euclidean distance between
        the ith point in P and the jth point in S.
    '''

    # verify input and get number of rows/columns
    assert P.ndim == S.ndim == 2, 'P and S must be 2D arrays'
    # determine dimensions of data matrices P and S
    Mp,Np = P.shape
    Ms,Ns = S.shape
    assert Mp == Ms == 3, 'P and S must have 3 rows'

    # initialize squared EDM D
    D = np.zeros((Np,Ns))

    # iterate over all rows of D
    for i in range(Np):
        for j in range(Ns):
            d = P[:,i] - S[:,j]
            D[i,j] = np.dot(d, d)
    return D

def _check_input(P, S):
    '''
    Verify input validity.

    parameters
    ----------
    P: numpy array 2D
        3 x N matrix containing the coordinates x (1rt row), y (2nd row),
        z (3rd row) of N points. The ith column represents the ith point.
    S: numpy array 2D
        3 x M matrix containing the coordinates x (1rt row), y (2nd row),
        z (3rd row) of M points. The ith column represents the ith point.

    returns
    -------
    Mp, Np, Ms, Ns: integers
        Number of rows (Mp, Ms) and columns (Np, Ns) of P and S.
    '''
    assert P.ndim == S.ndim == 2, 'P and S must be 2D arrays'
    # determine dimensions of data matrices P and S
    Mp,Np = P.shape
    Ms,Ns = S.shape
    assert Mp == Ms == 3, 'P and S must have 3 rows'

    return Mp, Np, Ms, Ns


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

# Gradient descent optimization
# The learning rate is specified by eta
class GDOptimizer(object):
    def __init__(self, eta):
        self.eta = eta

    def initialize(self, layers):
        pass

    # This function performs one gradient descent step
    # layers is a list of dense layers in the network
    # g is a list of gradients going into each layer before the nonlinear activation
    # a is a list of outputs from previous layer
    def update(self, layers, g, a):
        m = a[0].shape[1]
        for layer, curGrad, curA in zip(layers, g, a):
            # TODO: PART E ###############################################################
            # PART E #####################################################################
            layer.W -= self.eta * np.dot(curGrad, curA.T) / m
            layer.b -= self.eta * np.mean(curGrad, axis=1).reshape((-1,1))

# Cost function used to compute prediction errors
class QuadraticCost(object):

    # Compute the squared error between the prediction yp and the observation y
    # This method should compute the cost per element such that the output is the
    # same shape as y and yp
    @staticmethod
    def fx(y,yp):
        # TODO: PART B ###################################################################
        # PART B #########################################################################
        return np.square(np.subtract(y,yp)) / 2.0

    # Derivative of the cost function with respect to yp
    @staticmethod
    def dx(y,yp):
        # TODO: PART B ###################################################################
        # PART B #########################################################################
        return np.subtract(yp,y)

# Sigmoid function fully implemented as an example
class SigmoidActivation(object):
    @staticmethod
    def fx(z):
        # PART C Example #################################################################
        return 1 / (1 + np.exp(-z))
        # PART C #########################################################################

    @staticmethod
    def dx(z):
        # PART C Example #################################################################
        # PART C #########################################################################
        return np.multiply(fx(z), 1-fx(z))

# Hyperbolic tangent function
class TanhActivation(object):

    # Compute tanh for each element in the input z
    @staticmethod
    def fx(z):
        # TODO: PART C ###################################################################
        # PART C #########################################################################
        return np.tanh(z)

    # Compute the derivative of the tanh function with respect to z
    @staticmethod
    def dx(z):
        # TODO: PART C ###################################################################
        # PART C #########################################################################
        return 1 - np.square(np.tanh(z))

# Rectified linear unit
class ReLUActivation(object):
    @staticmethod
    def fx(z):
        # TODO: PART C ###################################################################
        # PART C #########################################################################
        return z * (z>0)

    @staticmethod
    def dx(z):
        # TODO: PART C ###################################################################
        # PART C #########################################################################
        return z > 0

# Linear activation
class LinearActivation(object):
    @staticmethod
    def fx(z):
        # TODO: PART C ###################################################################
        # PART C #########################################################################
        return z

    @staticmethod
    def dx(z):
        # TODO: PART C ###################################################################
        # PART C #########################################################################
        return np.ones(z.shape)

# This class represents a single hidden or output layer in the neural network
class DenseLayer(object):

    # numNodes: number of hidden units in the layer
    # activation: the activation function to use in this layer
    def __init__(self, numNodes, activation):
        self.numNodes = numNodes
        self.activation = activation

    def getNumNodes(self):
        return self.numNodes

    # Initialize the weight matrix of this layer based on the size of the matrix W
    def initialize(self, fanIn, scale=1.0):
        s = scale * np.sqrt(6.0 / (self.numNodes + fanIn))
        self.W = np.random.normal(0, s, (self.numNodes,fanIn))
        self.b = np.random.uniform(-1, 1, (self.numNodes,1))

    # Apply the activation function of the layer on the input z
    def a(self, z):
        return self.activation.fx(z)

    # Compute the linear part of the layer
    # The input a is an n x k matrix where n is the number of samples
    # and k is the dimension of the previous layer (or the input to the network)
    def z(self, a):
        return self.W.dot(a) + self.b # Note, this is implemented where we assume a is k x n

    # Compute the derivative of the layer's activation function with respect to z
    # where z is the output of the above function.
    # This derivative does not contain the derivative of the matrix multiplication
    # in the layer.  That part is computed below in the model class.
    def dx(self, z):
        return self.activation.dx(z)

    # Update the weights of the layer by adding dW to the weights
    def updateWeights(self, dW):
        self.W = self.W - dW

    # Update the bias of the layer by adding db to the bias
    def updateBias(self, db):
        self.b = self.b - db

# This class handles stacking layers together to form the completed neural network
class Model(object):

    # inputSize: the dimension of the inputs that go into the network
    def __init__(self, inputSize):
        self.layers = []
        self.inputSize = inputSize

    # Add a layer to the end of the network
    def addLayer(self, layer):
        self.layers.append(layer)

    # Get the output size of the layer at the given index
    def getLayerSize(self, index):
        if index >= len(self.layers):
            return self.layers[-1].getNumNodes()
        elif index < 0:
            return self.inputSize
        else:
            return self.layers[index].getNumNodes()

    # Initialize the weights of all of the layers in the network and set the cost
    # function to use for optimization
    def initialize(self, cost, initializeLayers=True):
        self.cost = cost
        if initializeLayers:
            for i in range(0,len(self.layers)):
                if i == len(self.layers) - 1:
                    self.layers[i].initialize(self.getLayerSize(i-1))
                else:
                    self.layers[i].initialize(self.getLayerSize(i-1))

    # Compute the output of the network given some input a
    # The matrix a has shape n x k where n is the number of samples and
    # k is the dimension
    # This function returns
    # yp - the output of the network
    # a - a list of inputs for each layer of the newtork where
    #     a[i] is the input to layer i
    #     (note this does not include the network output!)
    # z - a list of values for each layer after evaluating layer.z(a) but
    #     before evaluating the nonlinear function for the layer
    def evaluate(self, x):
        curA = x.T
        a = [curA]
        z = []
        for layer in self.layers:
            z.append(layer.z(curA))
            curA = layer.a(z[-1])
            a.append(curA)
        yp = a.pop()
        return yp, a, z

    # Compute the output of the network given some input a
    # The matrix a has shape n x k where n is the number of samples and
    # k is the dimension
    def predict(self, a):
        a, _, _ = self.evaluate(a)
        return a.T

    # Computes the gradients at each layer. y is the true labels, yp is the
    # predicted labels, and z is a list of the intermediate values in each
    # layer. Returns the gradients and the forward pass outputs (per layer).
    #
    # In particular, we return a list of dMSE/dz_i. The reasoning behind this is that
    # in the update function for the optimizer, we do not give it the z values
    # we compute from evaluating the network.
    def compute_grad(self, x, y):
        # Feed forward, computing outputs of each layer and
        # intermediate outputs before the non-linearities
        yp, a, z = self.evaluate(x)

        # d is inialized here to be (dMSE / dyp)
        d = self.cost.dx(y.T, yp)
        grad = []
        # Backpropogate the error
        for layer, curZ in zip(reversed(self.layers), reversed(z)):
            # TODO: PART D ###################################################################
            # grad[i] should correspond with the gradient of the output of layer i before the
            # activation is applied (dMSE / dz_i); be sure values are stored in the correct
            # ordering!
            # PART D #########################################################################
            grad.append(d * layer.dx(curZ))
            d = layer.W.T @ grad[-1]
        grad = reversed(grad)
        return grad, a

    # Train the network given the inputs x and the corresponding observations y
    # The network should be trained for numEpochs iterations using the supplied
    # optimizer
    def train(self, x, y, numEpochs, optimizer):

        # Initialize some stuff
        n = x.shape[0]
        x = x.copy()
        y = y.copy()
        hist = []
        optimizer.initialize(self.layers)

        # Run for the specified number of epochs
        for epoch in range(0,numEpochs):

            # Compute the gradients
            grad, a = self.compute_grad(x, y)

            # Update the network weights
            optimizer.update(self.layers, grad, a)

            # Compute the error at the end of the epoch
            yh = self.predict(x)
            C = self.cost.fx(y, yh)
            C = np.mean(C)
            hist.append(C)
        return hist

    def trainBatch(self, x, y, batchSize, numEpochs, optimizer):
        # Copy the data so that we don't affect the original one when shuffling
        x = x.copy()
        y = y.copy()
        hist = []
        n = x.shape[0]

        for epoch in np.arange(0,numEpochs):

            # Shuffle the data
            r = np.arange(0,x.shape[0])
            x = x[r,:]
            y = y[r,:]
            e = []

            # Split the data in chunks and run SGD
            for i in range(0,n,batchSize):
                end = min(i+batchSize,n)
                batchX = x[i:end,:]
                batchY = y[i:end,:]
                e += self.train(batchX, batchY, 1, optimizer)
            hist.append(np.mean(e))

        return hist

if __name__ == '__main__':
    N0 = 3
    N1 = 9

    # Switch these statements to True to run the code for the corresponding parts
    # Part E
    SGD = True
    # Part F
    DIFF_SIZES = True

    # Generate the training set
    np.random.seed(9001)
    y_train = mnist.train_labels()
    y_test = mnist.test_labels()
    X_train = (mnist.train_images()/255.0)
    X_test = (mnist.test_images()/255.0)
    train_idxs = np.logical_or(y_train == N0, y_train == N1)
    test_idxs = np.logical_or(y_test == N0, y_test == N1)
    y_train = y_train[train_idxs].astype('int')
    y_test = y_test[test_idxs].astype('int')
    X_train = X_train[train_idxs]
    X_test = X_test[test_idxs]
    y_train = (y_train == N0).astype('int')
    y_test = (y_test == N0).astype('int')
    y_train *= 2
    y_test *= 2
    y_train -= 1
    y_test -= 1
    X_train = X_train.reshape(X_train.shape[0], -1)
    X_test = X_test.reshape(X_test.shape[0], -1)
    y_test = y_test[:, np.newaxis]
    y = y_train[:, np.newaxis]
    x = X_train
    xLin=np.linspace(-np.pi,np.pi,250).reshape((-1,1))

    yHats = {}

    activations = dict(ReLU=ReLUActivation,
                       tanh=TanhActivation,
                       linear=LinearActivation)
    lr = dict(ReLU=0.02,tanh=0.02,linear=0.005)
    names = ['ReLU','linear','tanh']

    #### PART E ####
    if SGD:
        print('\n----------------------------------------\n')
        print('Using SGD')
        for key in names[1:2]:
            for batchSize in [10, 50, 100, 200]:
                for epoch in [10, 20, 40]:
                    activation = activations[key]
                    model = Model(x.shape[1])
                    model.addLayer(DenseLayer(4,activation()))
                    model.addLayer(DenseLayer(1,LinearActivation()))
                    model.initialize(QuadraticCost())
                    hist = model.trainBatch(x, y, batchSize, epoch, GDOptimizer(eta=lr[key]))
                    y_hat_train = model.predict(x)
                    y_pred_train = np.sign(y_hat_train)
                    y_hat_test = model.predict(X_test)
                    y_pred_test = np.sign(y_hat_test)
                    error_train = np.mean(np.square(y_hat_train - y))/2
                    error_test = np.mean(np.square(y_hat_test - y_test))/2

                    print('Batch size: ', batchSize ,'Epoch: ', epoch, 'Train Error: ', error_train, 'Test Error: ', error_test)

    #### PART F ####
    # Train with different sized networks
    if DIFF_SIZES:
        print('\n----------------------------------------\n')
        print('Training with various sized network')
        names = ['ReLU', 'tanh']
        widths = [2, 4, 8, 16, 32]
        errors = {}
        for key in names[1:]:
            error = []
            for width in widths:
                activation = activations[key]
                model = Model(x.shape[1])
                model.addLayer(DenseLayer(width,activation()))
                model.addLayer(DenseLayer(1,LinearActivation()))
                model.initialize(QuadraticCost())
                epochs = 256
                hist = model.trainBatch(x,y,x.shape[0],epochs,GDOptimizer(eta=lr[key]))

                y_hat_train = model.predict(x)
                y_hat_test = model.predict(X_test)

                error_train = np.mean(np.square(y_hat_train - y))/2
                error_test = np.mean(np.square(y_hat_test - y_test))/2

                print(key+' neural nework width('+str(width)+') train MSE: '+str(error_train))
                print(key+' neural network width('+str(width)+') test MSE: '+str(error_test))

if DIFF_SIZES:
    print('\n----------------------------------------\n')
    print('Training with various sized network')
    names = ['ReLU', 'tanh']
    widths = [2, 4, 8, 16, 32]
    errors = {}
    for key in names[1:]:
        error = []
        for width in widths:
            activation = activations[key]
            model = Model(x.shape[1])
            model.addLayer(DenseLayer(width,activation()))
            model.addLayer(DenseLayer(1,LinearActivation()))
            model.initialize(QuadraticCost())
            epochs = 256
            hist = model.trainBatch(x,y,x.shape[0],epochs,GDOptimizer(eta=lr[key]))

            y_hat_train = model.predict(x)
            y_hat_test = model.predict(X_test)

            error_train = np.mean(np.square(y_hat_train - y))/2
            error_test = np.mean(np.square(y_hat_test - y_test))/2

            print(key+' neural nework width('+str(width)+') train MSE: '+str(error_train))
            print(key+' neural network width('+str(width)+') test MSE: '+str(error_test))


import numpy as np
import scipy.signal as ssi

def corr_fun(X, Y, dt, norm=True, biased=True, method='auto'):
    """
    Estimates the correlation function between X and Y using ssi.correlate.
    For now, we require both signals to be of equal length.

    Input:
        X: First array to be correlated. ............................. (Nx1) np.array
        Y: Second array to be correlated. ............................ (Nx1) np.array
        dt: Time step of the time series. ............................ float
        norm: Normalizes the correlation function to a maxima of 1 ... bool
        biased: Trigger estimator biasing. ........................... bool
        method: 'direct', 'fft' or 'auto'. Passed to ssi.correlate ... string

    For biased=True, the result is divided by X.size.
    For biased=False, the estimator is unbiased and returns the result
    divided by X.size-|k|, where k is the lag.
    The unbiased estimator diverges for large lags, and
    for small lags and large X.size, the difference is trivial.
    """

    assert(X.size==Y.size)

    if norm:
        Xn = (X-X.mean())/X.std()
        Yn = (Y-Y.mean())/Y.std()
    else:
        Xn = X
        Yn = Y

    R = ssi.correlate(Xn, Yn, mode='full', method = method)

    k = np.arange(-(X.size-1), X.size)
    tb = k*dt

    if biased:
        R /= X.size
    else:
        R /= (X.size-np.abs(k))

    return tb, R

# End of file correlation_function.py


# Importing the dependancies
import numpy as np
import matplotlib.pyplot as plt

# Creating random data with 3 input features and 2 output features
X = np.random.randn(3, 1000)
y = np.random.randint(2, size=1000)

# Defining the sigmoid function
def sigmoid(z):
    return 1 / (1 + np.exp(-z))

def forward_prop(X, y, params):

    """"
    Propagate forward by computing:
    A = sigmoid(W.X + b)

    Arguments:
    X -- input data (num_of_features, training_size)
    y -- input data (1, training_size)
    W -- weights (num_of_features, 1)
    b -- scalar value

    Returns:
    A -- the prediction
    cost -- the cost function
    """

    m = X.shape[1]

    Z = np.dot(params['W'].T, X) + params['b']
    A = sigmoid(Z)

    cost = -(1/m)*np.sum((y*np.log(A) + (1-y)*np.log(1-A)))

    return A, cost

def back_prop(X, A):

    """
    Propagate backwards by computing the gradients:
    dA = -(y/A) + (1-y)/(1-A)
    dZ = A - y
    dW = (1/m)*X.dZ
    db = (1/m)*sum(dZ)

    Arguments:
    X -- input data (num_of_features, training_size)
    A -- the prediction

    Returns:
    dW, db -- gradients with same shape as W, b respectively
    """

    m = X.shape[1]

    dZ = A - y
    dW = (1/m)*np.dot(X, dZ.T)
    db = (1/m)*np.sum(dZ)

    grads = {'dW': dW, 'db': db}

    return grads

def update_parameters(params, grads, learning_rate=0.001):

    """
    Update the parameters:
    W = W - lr*dW
    b = b - lr*db

    Arguments:
    W -- weights (num_of_features, 1)
    b -- scalar value
    dW, db -- gradients
    learning_rate -- the rate at which the params should update (default = 0.001)

    Returns:
    W, b -- updated parameters
    """

    params['W'] -= learning_rate*grads['dW']
    params['b'] -= learning_rate*grads['db']

    return params

def model(X, y, epochs, learning_rate=0.001, visualize=False):

    """
    Logistic Regression model

    Arguments:
    X -- input data (num_of_features, training_size)
    y -- input data (1, training_size)
    learning_rate -- the rate at which the params should update (default = 0.001)
    epochs -- number of iterations
    visualize -- represent the loss on a graph (default = False)

    Returns:
    W, b -- parameters
    """

    # initialise the parameters
    W = np.zeros((X.shape[0], 1))
    b = 0

    params = {'W': W, 'b': b}

    # initialise the gradients
    dW = np.zeros((X.shape[0], 1))
    db = 0

    grads = {'dW': dW, 'db': db}

    cost = [0]*epochs

    # running the algorithm
    for i in range(epochs):

        # forward prop
        A, cost[i] = forward_prop(X, y, params)

        # printing the cost
        if i%100 == 0:
            print(f'loss after {i} iterations = {cost[i]}')

        # backwards prop
        grads = back_prop(X, A)

        # updating the params
        params = update_parameters(params, grads, learning_rate)

    if visualize:

        plt.style.use('fivethirtyeight')

        plt.plot(cost, label = 'cost')
        plt.xlabel('Epochs')
        plt.ylabel('Cost')
        plt.legend()
        plt.show()

    return params

# Calling the function
p = model(X, y, 10000, visualize=True)


from time import time
from pick import pick

# Functions

def f(x):
	return eval(function_string)

def LeftRiemannSums(j):
	sum=0
	for i in range(j):
		sum+=f((i/j)*(end-start)+start)*((end-start)/j)
#		print(sum)
	return sum
def RightRiemannSums(j):
	sum=0
	for i in range(1,j+1,1):
		sum+=f((i/j)*(end-start)+start)*((end-start)/j)
#		print(sum)
	return sum

def TrapezoidalSums(j):
	sum=0
	for i in range(j):
		sum+=0.5*( f((i/j)*(end-start)+start) + f(((i+1)/j)*(end-start)+start) )*((end-start)/(j))
#		print(sum)
	return sum

def MonteCarlo1(j,k): # j=number of slots, k=number of points per slot
	in_dots=0
	out_dots=0
	space=linspace(start,end,j)
	y=vf(space)
	for i in space:
		for l in range(k):
			if random.uniform(0.,max(y))<=f(i):
				in_dots+=1
			else:
				out_dots+=1
	return (in_dots/(out_dots+in_dots))*max(y)*(end-start)

def MonteCarlo2(j,k=100):
	in_d=0
	out_d=0
	h=max(vf(linspace(start,end,k)))
	p=0
	for i in range(j):
		p=[random.uniform(start,end),np.random.uniform(0,h)]
		if p[1]<=f(p[0]):
			in_d+=1
		else:
			out_d+=1
	return (in_d/(out_d+in_d))*h*(end-start)

def exclude(ex,n,strt=0,stp=1):
	'''
	Gives an Integer iterator [0,ex)U(ex,n)
	'''
	return [q for q in range(strt,n,stp) if q!=ex]

def PC(j):
	'''
	Polinomial Coefficient
	'''
	p=1
	for m in exclude(j,k):
		p*=(x-xp[m])/(xp[j]-xp[m])
	return p

def LS():
	'''
	Lagrange Sums
	'''
	p=0
	for i in range(k):
		p+=yp[i]*PC(i)
	return p

# Inputs

print("Demo to numerically integrate numpy real valued functions. \nAll functions have to be written with the x variable. ")
function_string=input("Function: ")
start=float(input("From: "))
end=float(input("To: "))
if (start==end):
	print('\n\n',0)
	quit()

# Pick Method

title = 'Select an Integration Method: '
options = ['Riemman sums','Trapezoid Rule','Monte Carlo 1','Monte Carlo 2',"SymPy Symbolic Integration", "Lagrange Polynomial Interpolation"]
method, indexp = pick(options, title)
print('Method Selected: %s'%(method))

# Riemann sums

if (method=='Riemman sums'):
	from numpy import *
	n=int(input('Number of partitions for the Riemann sums: '))
	print('')
	t1=time()
	print('Left Riemann Sums:  %f'%(LeftRiemannSums(n)))
	print('Right Riemann Sums: %f'%(RightRiemannSums(n)))
	print('Total calculation time: %f'%(time()-t1))

# Trapezoid Rule

if (method=='Trapezoid Rule'):
	from numpy import *
	n=int(input('Number of partitions for the Trapezoid Rule: '))
	print('')
	t1=time()
	print('Result: %f'%(TrapezoidalSums(n)))
	print('Total calculation time: %f'%(time()-t1))

# Monte Carlo 1

if (method=='Monte Carlo 1'):
	from numpy import *
	vf=vectorize(f)
	n=int(input('Number of partitions for Monte Carlo: '))
	m=int(input('Number of points per partition: '))
	print('')
	t1=time()
	print('Result: %f'%(MonteCarlo1(n,m)))
	print('Total calculation time: %f'%(time()-t1))

# Monte Carlo 2

if (method=='Monte Carlo 2'):
	from numpy import *
	vf=vectorize(f)
	n=int(input('Number of dots for Monte Carlo: '))
	print('')
	t1=time()
	print('Result: %f'%(MonteCarlo2(n)))
	print('Total calculation time: %f'%(time()-t1))

# SymPy

if (method=="SymPy Symbolic Integration"):
	from sympy import *
	x=symbols('x')
	g=eval(function_string)
	t1=time()
	G=integrate(g,x)
	print("Indefinite integral",G)
	print("Definite integral",G.subs(x,end)-G.subs(x,start))
	print("Total calculation time: %f"%(time()-t1))

# Lagrange

if (method=="Lagrange Polynomial Interpolation"):
	from sympy import *
	x=symbols('x')
	g=eval(function_string)
	t1=time()

	k=int(input("Degree of each Lagrange Polynomial Interpolation: "))
	from numpy import linspace,vectorize
	xp=linspace(start,end,k+1)
	def gfunc(x):
		return eval(str(g))
	vgfunc=vectorize(gfunc)
	yp=vgfunc(xp)
	pol_string=str(expand(LS()))
	def pol(x):
		return eval(pol_string)
	import matplotlib.pyplot as plt
	space=linspace(start,end,100)
	plt.figure()
	plt.plot(space,vgfunc(linspace(start,end,100)))
	plt.plot(space,list(map(pol,space)))
	plt.scatter(xp,list(map(pol,xp)))
	plt.show()


import numpy as np
import matplotlib.pyplot as plt

#ma+bv+kx=0 (no out force)
#define: (b/m)=r , (k/m)=w^2
#x''+rx'+(w^2)x=0
#dx/dt=v, dv/dt=-rv-(w^2)x
#define: tau=wt
#dx/dtau=v/w, dv/dtau=-(r/w)v-wx
#define: u=v/w
#dx/dtau=u
#du/dtau=-(r/w)u-x
#let y1=x, y2=u
#dy1/dtau=y2=g1, dy2/dtau=-(r/w)y2-y1=g2

#parameter:
gamma=[0.1,1,2,3] #damping coefficient
omega_0=1.0 # natural frequency
N=2000
h=0.01
t=np.arange(N)*h

#initial condition: x(0)=X0, v(0)=0
x0=1
v0=0

def g1(y1,y2):
    return (y2)

def g2(y1,y2,gamma_i):
    return (-y1-(gamma_i/omega_0)*y2)

#calculate the value of the next moment by RK2 method
def y_rk2(y1,y2,gamma_i):#with (w1,w2,alpha,beta) = (0.5,0.5,1,1)
    k1=g1(y1,y2)
    q1=g2(y1,y2,gamma_i)
    k2=g1(y1+h*k1,y2+h*q1)
    q2=g2(y1+h*k1,y2+h*q1,gamma_i)
    y1_prime=y1+h*(0.5*k1+0.5*k2)
    y2_prime=y2+h*(0.5*q1+0.5*q2)
    return y1_prime,y2_prime

def SHO(gamma_i):
    x=[]
    v=[]
    x.append(x0)
    v.append(v0)
    for i in range(0,N-1):
        a,b=y_rk2(x[i],v[i],gamma_i)
        x.append(a)
        v.append(b)
    return x,v

def visualize(x,v,gamma_i):
    plt.plot(t/np.pi, x, label="x(t)")
    plt.plot(t/np.pi, v, label="v(t)")
    plt.axhline(y=1, linestyle="--")
    plt.axhline(y=-1, linestyle="--")
    plt.title("RK2, SHO with $\gamma/\omega0=$%1.1f, x(0)=%1.1f,v(0)=%1.1f"%(gamma_i/omega_0, x0, v0))
    plt.xlabel("$t/\pi$")
    plt.ylabel("$y_n(t)$")
    plt.legend()
    plt.show()

#main function:
for j in gamma:
    x,v=SHO(j)
    visualize(x,v,j)


# -*- coding: utf-8 -*-
"""
Created on Mon Dec 13 15:32:20 2021

@author: Oscar
"""
"Tutorial 5 Numerical Simulation of chaos, the Lorenz System"
from scipy.integrate import solve_ivp
import numpy as np
import math
import matplotlib.pyplot as plt

# Define Function models
def model(t, y):
    rho = 28
    beta = 8/3
    sigma = 10

    y_1 = y[0]
    y_2 = y[1]
    y_3 = y[2]

    f1 = sigma * (y_2 - y_1)
    f2 = rho * y_1 - y_2 - y_1 * y_3
    f3 = -beta * y_3 + y_1 * y_2

    return [f1, f2, f3]

# Define Function models
def model2(t, y):
    rho = 10
    beta = 8/3
    sigma = 10

    y_1 = y[0]
    y_2 = y[1]
    y_3 = y[2]

    g1 = sigma * (y_2 - y_1)
    g2 = rho * y_1 - y_2 - y_1 * y_3
    g3 = -beta * y_3 + y_1 * y_2

    return [g1, g2, g3]

# Define initial Conditions
t = 0
t_f = 30

y0_1 = 5
y0_2 = 5
y0_3 = 5

# Define time interval
t_eval = np.linspace(t, t_f, num= 5000)

# Solution
y = solve_ivp(model, [t, t_f], [y0_1, y0_2, y0_3], t_eval= t_eval)
z = solve_ivp(model2, [t, t_f], [y0_1, y0_2, y0_3], t_eval= t_eval)
# ------------------------------------------------------
# Plot Results
plt.figure(1)
plt.plot(y.t, y.y[0,:] , 'b-', y.t,y.y[1,:],
         'r-', y.t, y.y[2,:], 'g-')
plt.xlabel('t')
plt.ylabel('y_1(t), y_2(t), y_3(t)')
# ------------------------------------------------------

# plot results
plt.figure(2)
plt.plot(y.y[0,:] ,y.y[1,:],'-')
plt.xlabel('y_1')
plt.ylabel('y_2')
# ------------------------------------------------------

# plot results
plt.figure(3)
plt.plot(y.y[0,:] ,y.y[2,:],'-')
plt.xlabel('y_1')
plt.ylabel('y_3')
# ------------------------------------------------------

plt.show()
# ------------------------------------------------------
# Plot Results
plt.figure(4)
plt.plot(z.t, z.y[0,:] , 'b-', z.t,z.y[1,:],
         'r-', z.t, z.y[2,:], 'g-')
plt.xlabel('t')
plt.ylabel('y_1(t), y_2(t), y_3(t)')
# ------------------------------------------------------

# plot results
plt.figure(5)
plt.plot(z.y[0,:] ,z.y[1,:],'-')
plt.xlabel('y_1')
plt.ylabel('y_2')
# ------------------------------------------------------

# plot results
plt.figure(6)
plt.plot(z.y[0,:] ,z.y[2,:],'-')
plt.xlabel('y_1')
plt.ylabel('y_3')

# print results in a text file (for later use if needed)
file_name= 'output.dat'
f_io = open(file_name,'w')
n_step = len(y.t)
for i in range(n_step):
    s1 = str(i)
    s2 = str(y.t[i])
    s3 = str(y.y[0,i])
    s4 = str(y.y[1,i])
    s_tot = s1 + ' ' + s2 + ' ' + s3  + ' ' + s4
    f_io.write(s_tot + '\n')
f_io.close()
# ------------------------------------------------------


import numpy as np
from scipy.linalg import logm

""" UTILITY FUNCTIONS """

def hat(w):
    """ Function takes in a vector of size 3 and returns
    its corresponding skew-symmetric matrix """
    w1 = w[0]
    w2 = w[1]
    w3 = w[2]
    what  = np.array( [ [0,-w3,w2], [w3,0,-w1], [-w2,w1,0] ] )
    return what

def unhat(what):
    """ Function takes in a skew-symmetric matrix and returns
    its corresponding vector """
    w1 = what[2,1]
    w2 = what[0,2]
    w3 = what[1,0]
    w  = np.array( (w1,w2,w3) )
    return w

def qmult(q1,q2):
    """ Function takes in quaternions q1 and q2, and performs
    quaternion multiplication:  q3 = q1*q2 """
    v1 = q1[0:3]
    s1 = q1[3]
    q3 = np.block([ [s1*np.identity(3) + hat(v1), v1[:,np.newaxis] ], [-v1, s1] ]) @ q2
    return q3

def qconj(q):
    """ Function takes in a quaternion and returns its conjugate """
    v = q[0:3]
    v = -v
    qplus = np.concatenate((v,q[3,np.newaxis]),axis=0)
    return qplus

def phi_to_quat(phi):
    """ Function takes in a rotation parameterized by
    Euler Axis & Angle and returns its corresponding quaternion """
    if np.linalg.norm(phi) > 10*np.pi/180:
        theta = np.linalg.norm(phi)
        r     = phi/theta
        qvec  = r*np.sin(theta/2)
        qsca  = np.array(np.cos(theta/2))
        q     = np.hstack((qvec,qsca))
    else:
        qvec  = phi/2
        qsca  = np.array(1-1/8*np.dot(phi,phi))
        q     = np.hstack((qvec,qsca))
    return q

def quat_to_phi(q):
    """ Function takes in a rotation parameterized by
    a quaternion and returns its corresponding Euler Axis & Angle """
    Q = quat_to_rot(q)
    phi = unhat(logm(Q))
    return phi

def quat_to_rot(q):
    """ Function takes in a rotation parameterized by
    a quaternion and returns its corresponding rotation matrix """
    v = q[0:3]
    s  = q[3]
    A = np.identity(3) + 2*hat(v) @ (s*np.identity(3) + hat(v))
    #A = np.transpose(A)
    return A

""" Below is another way to convert from quaternion to rotation matrix
def quat_to_rot(q):
    q1 = q[0]
    q2 = q[1]
    q3 = q[2]
    q4 = q[3]
    Q  = np.array( [ [0,-q3,+q2], [+q3,0,-q1], [-q2,q1,0] ] )
    A  = (q4**2 - (q1**2+q2**2+q3**2))*np.identity(3) + 2*np.outer(np.array([q1,q2,q3]), np.array([q1,q2,q3])) - 2*q4 * Q
    return A
"""


import numpy as np

class Complejo():
    def __init__(self,real,imag):
        self.real = real
        self.imaginario = imag
        self.norma = np.sqrt(self.real**2 + self.imaginario**2)
    def conjugado(self):
        self.imaginario = -self.imaginario
    def calcula_norma(self):
        self.norma = np.sqrt(self.real**2 + self.imaginario**2)
    def pow(self,n):
        resultado = Complejo(self.real,self.imaginario)
        for i in range(n-1):
            resultado = Complejo(self.real*resultado.real - self.imaginario*resultado.imaginario,self.imaginario*resultado.real + self.real*resultado.imaginario)
        return(resultado)