import math
import numpy as np

# -------------------------------------------------------------------------
'''
    Problem 2: Logistic Regression
    In this problem, you will implement the logistic regression method for binary classification problems.
    The main goal of this problem is to get familiar with a model-based classification method, and how to train the model parameters on the training data.
    We will get familiar with gradient computation using the chain rule.
    We will use cross entropy as the loss function and stochastic gradient descent to train the model parameters.
    You could test the correctness of your code by typing `nosetests test2.py` in the terminal.

    Notations:
            ---------- input data ----------------------
            p: the number of input features.
            x: the feature vector of a data instance, a float numpy matrix of shape p by 1.
            y: the label of a training instance, an integer scalar value. The values can be 0 or 1.

            ---------- model parameters ----------------------
            w: the weights parameter of the logistic model, a float numpy matrix of shape p by 1.
            b: the bias parameter of the logistic model, a float scalar.

            ---------- values ----------------------
            z: the logit value, a float scalar
            a: the activation value, a float scalar
            L: the cross entropy loss value, a float scalar.

            ---------- partial gradients ----------------------
            dL_da: the partial gradient of the loss function L w.r.t. the activation a, a float scalar value. It represents (d_L / d_a)
            da_dz: the partial gradient of the activation a w.r.t. the logit z, a float scalar value. It represents (d_a / d_z)
            dz_dw: the partial gradients of the logit z w.r.t. the weights w, a numpy float matrix of shape (p by 1). It represents (d_z / d_w)
                   The i-th element represents ( d_z / d_w[i])
            dz_db: the partial gradient of logit z w.r.t. the bias b, a float scalar. It represents (d_z / d_b).

            ---------- partial gradients of parameters ------------------
            dL_dw: the partial gradient of the loss function L w.r.t. the weight vector w, a numpy float matrix of shape (p by 1).
                   The i-th element represents ( d_L / d_w[i])
            dL_db: the partial gradient of the loss function L w.r.t. the bias b, a float scalar.

            ---------- training ----------------------
            alpha: the step-size parameter of gradient descent, a float scalar.
            n_epoch: the number of passes to go through the training dataset in the training process, an integer scalar.
'''

# -----------------------------------------------------------------
# Forward Pass
# -----------------------------------------------------------------

# --------------------------
def compute_z(x, w, b):
    '''
        Compute the linear logit value of a data instance. z = <w, x> + b
        Here <w, x> represents the dot product of the two vectors.
        Input:
            x: the feature vector of a data instance, a float numpy matrix of shape p by 1.
            w: the weights parameter of the logistic model, a float numpy matrix of shape p by 1.
            b: the bias value of the logistic model, a float scalar.
        Output:
            z: the logit value of the instance, a float scalar
        Hint: you could solve this problem using 1 line of code. Though using more lines of code is also okay.
    '''
    #########################################
    ## INSERT YOUR CODE HERE
    z = np.linalg.det(np.dot(w.T, x)) + b
    #########################################
    return z

# --------------------------
def compute_a(z):
    '''
        Compute the sigmoid activation.
        Input:
            z: the logit value of logistic regression, a float scalar.
        Output:
            a: the activation, a float scalar
    '''
    #########################################
    ## INSERT YOUR CODE HERE
    # TODO: Math Range Error! Ask TA
    a = (1 / (1 + math.exp(-z)))
    #########################################
    return a

# --------------------------
def compute_L(a, y):
    '''
        Compute the loss function: the negative log likelihood, which is the negative logarithm of the likelihood.
        This function is also called cross-entropy.
        Input:
            a: the activation of a training instance, a float scalar
            y: the label of a training instance, an integer scalar value. The values can be 0 or 1.
        Output:
            L: the loss value of logistic regression, a float scalar.
    '''
    #########################################
    ## INSERT YOUR CODE HERE

    #########################################
    return L

# --------------------------
def forward(x, y, w, b):
    '''
       Forward pass: given an instance in the training data, compute the logit z, activation a and cross entropy L on the instance.
        Input:
            x: the feature vector of a training instance, a float numpy matrix of shape p by 1. Here p is the number of features/dimensions.
            y: the label of a training instance, an integer scalar value. The values can be 0 or 1.
            w: the weight vector, a float numpy matrix of shape p by 1.
            b: the bias value, a float scalar.
        Output:
            z: linear logit of the instance, a float scalar
            a: activation, a float scalar
            L: the cross entropy loss on the training instance, a float scalar.
        Hint: you could solve this problem using 3 lines of code. Though using more lines of code is also okay.
    '''
    #########################################
    ## INSERT YOUR CODE HERE

    #########################################
    return z, a, L

# -----------------------------------------------------------------
# Compute Local Gradients
# -----------------------------------------------------------------

# --------------------------
def compute_dL_da(a, y):
    '''
        Compute local gradient of the cross-entropy function (the Loss function) L w.r.t. the activation a.
        Input:
            a: the activation value, a float scalar
            y: the label of a training instance, an integer scalar value. The values can be 0 or 1.
        Output:
            dL_da: the local gradient of the loss function w.r.t. the activation, a float scalar value.
    '''
    #########################################
    ## INSERT YOUR CODE HERE

    #########################################
    return dL_da

# --------------------------
def compute_da_dz(a):
    '''
        Compute local gradient of the sigmoid activation a w.r.t. the logit z.
        Input:
            a: the activation value of the sigmoid function, a float scalar
        Output:
            da_dz: the local gradient of the activation w.r.t. the logit z, a float scalar value.
        Hint: the gradient da_dz only depends on the activation a, instead of the logit z.
        Hint: you could solve this problem using 1 line of code.
    '''
    #########################################
    ## INSERT YOUR CODE HERE

    #########################################
    return da_dz

# --------------------------
def compute_dz_dw(x):
    '''
        Compute partial gradients of the logit function z with respect to (w.r.t.) the weights w.
        Input:
            x: the feature vector of a data instance, a float numpy matrix of shape p by 1.
               Here p is the number of features/dimensions.
        Output:
            dz_dw: the partial gradients of the logit z with respect to the weights w, a numpy float matrix of shape p by 1.
                   The i-th element represents ( d_z / d_w[i])
        Hint: you could solve this problem using 1 line of code.
    '''
    #########################################
    ## INSERT YOUR CODE HERE

    #########################################
    return dz_dw

# --------------------------
def compute_dz_db():
    '''
        Compute partial gradient of the logit function z with respect to (w.r.t.) the bias b.
        Output:
            dz_db: the partial gradient of logit z with respect to the bias b, a float scalar. It represents (d_z / d_b).
    '''
    #########################################
    ## INSERT YOUR CODE HERE

    #########################################
    return dz_db

# -----------------------------------------------------------------
# Back Propagation
# -----------------------------------------------------------------

# --------------------------
def backward(x, y, a):
    '''
       Back Propagation: given an instance in the training data, compute the local gradients for logit, activation, weights and bias on the instance.
        Input:
            x: the feature vector of a data instance, a float numpy matrix of shape p by 1.
            y: the label of a training instance, an integer scalar value. The values can be 0 or 1.
            a: the activation, a float scalar
        Output:
            dL_da: the local gradient of the loss function w.r.t. the activation, a float scalar value.
            da_dz: the local gradient of the activation a w.r.t. the logit z, a float scalar value. It represents ( d_a / d_z )
            dz_dw: the partial gradient of logit z with respect to the weight vector, a numpy float matrix of shape (p by 1).
                   The i-th element represents ( d_z / d_w[i])
            dz_db: the partial gradient of logit z with respect to the bias, a float scalar. It represents (d_z / d_b).
    '''
    #########################################
    ## INSERT YOUR CODE HERE

    #########################################
    return dL_da, da_dz, dz_dw, dz_db

# --------------------------
def compute_dL_dw(dL_da, da_dz, dz_dw):
    '''
       Given local gradients, compute the gradient of the loss function L w.r.t. the weights w.
        Input:
            dL_da: the local gradient of the loss function w.r.t. the activation, a float scalar value.
            da_dz: the local gradient of the activation a w.r.t. the logit z, a float scalar value. It represents ( d_a / d_z )
            dz_dw: the partial gradient of logit z with respect to the weight vector, a numpy float matrix of shape (p by 1).
                   The i-th element represents ( d_z / d_w[i])
        Output:
            dL_dw: the gradient of the loss function w.r.t. the weight vector, a numpy float matrix of shape (p by 1).
        Hint: you could solve this problem using 1 lines of code
    '''
    #########################################
    ## INSERT YOUR CODE HERE

    #########################################
    return dL_dw

# --------------------------
def compute_dL_db(dL_da, da_dz, dz_db):
    '''
       Given the local gradients, compute the gradient of the loss function L w.r.t. bias b.
        Input:
            dL_da: the local gradient of the loss function w.r.t. the activation, a float scalar value.
            da_dz: the local gradient of the activation a w.r.t. the logit z, a float scalar value. It represents ( d_a / d_z )
            dz_db: the partial gradient of logit z with respect to the bias, a float scalar. It represents (d_z / d_b).
        Output:
            dL_db: the gradient of the loss function w.r.t. the bias, a float scalar.
        Hint: you could solve this problem using 1 lines of code
    '''
    #########################################
    ## INSERT YOUR CODE HERE

    #########################################
    return dL_db

# -----------------------------------------------------------------
# gradient descent
# -----------------------------------------------------------------

# --------------------------
def update_w(w, dL_dw, alpha=0.001):
    '''
       Given an instance in the training data, update the weights w using gradient descent.
        Input:
            w: the current value of the weight vector, a numpy float matrix of shape p by 1.
            dL_dw: the gradient of the loss function w.r.t. the weight vector, a numpy float matrix of shape p by 1.
            alpha: the step-size parameter of gradient descent, a float scalar.
        Output:
            w: the updated weight vector, a numpy float matrix of shape p by 1.
        Hint: you could solve this problem using 1 line of code
    '''

    #########################################
    ## INSERT YOUR CODE HERE

    #########################################
    return w

# --------------------------
def update_b(b, dL_db, alpha=0.001):
    '''
       Given an instance in the training data, update the bias b using gradient descent.
        Input:
            b: the current value of bias, a float scalar.
            dL_db: the gradient of the loss function w.r.t. the bias, a float scalar.
            alpha: the step-size parameter of gradient descent, a float scalar.
        Output:
            b: the updated of bias, a float scalar.
        Hint: you could solve this problem using 1 line of code in the block.
    '''

    #########################################
    ## INSERT YOUR CODE HERE

    #########################################
    return b

# --------------------------
def train(X, Y, alpha=0.001, n_epoch=100):
    '''
       Given a training dataset, train the logistic regression model by iteratively updating the weights w and bias b using the gradients computed over each data instance.
We repeat n_epoch passes over all the training instances.
        Input:
            X: the feature matrix of training instances, a float numpy matrix of shape (n by p). Here n is the number of data instance in the training set, p is the number of features/dimensions.
            Y: the labels of training instance, a numpy integer matrix of shape n by 1. The values can be 0 or 1.
            alpha: the step-size parameter of gradient descent, a float scalar.
            n_epoch: the number of passes to go through the training set, an integer scalar.
        Output:
            w: the weight vector trained on the training set, a numpy float matrix of shape p by 1.
            b: the bias, a float scalar.
    '''

    # initialize weights and biases as 0
    w, b = np.mat(np.zeros(X.shape[1])).T, 0.

    for _ in range(n_epoch):
        for x, y in zip(X, Y):
            x = x.T  # convert to column vector
            #########################################
            ## INSERT YOUR CODE HERE
            # Forward pass: compute the logit, sigmoid activation and cross_entropy loss function.

            # Back propagation: compute local gradients

            # compute the global gradients using chain rule

            # update the parameters w and b

            #########################################
    return w, b

# --------------------------
def predict(Xtest, w, b):
    '''
       Predict the labels of the instances in a test dataset using logistic regression.
        Input:
            Xtest: the feature matrix of testing instances, a float numpy matrix of shape (n_test by p). Here n_test is the number of data instance in the test set, p is the number of features/dimensions.
            w: the weight vector of the logistic model, a float numpy matrix of shape p by 1.
            b: the bias value of the logistic model, a float scalar.
        Output:
            Y: the predicted labels of test data, an integer numpy array of length ntest.
                    If the predicted label is positive, the value is 1. If the label is negative, the value is 0.
            P: the predicted probability of test data to have positive labels, a float numpy matrix of shape ntest by 1.
                    Each value is between 0 and 1, indicating the probability of the instance having the positive label.
            Note: If the activation is 0.5, we consider the prediction as positive (instead of negative).
    '''
    n = Xtest.shape[0]
    Y = np.zeros(n)  # initialize as all zeros
    P = np.mat(np.zeros((n, 1)))
    for i, x in enumerate(Xtest):
        x = x.T  # convert to column vector
        #########################################
        ## INSERT YOUR CODE HERE

        #########################################
    return Y, P

# -----------------------------------------------------------------
# gradient checking
# -----------------------------------------------------------------

# --------------------------
def check_dL_da(a, y, delta=1e-7):
    '''
        Compute local gradient of the cross-entropy function w.r.t. the activation using gradient checking.
        Input:
            a: the activation value, a float scalar
            y: the label of a training instance, an integer scalar value. The values can be 0 or 1.
            delta: a small number for gradient check, a float scalar.
        Output:
            dL_da: the approximated local gradient of the loss function w.r.t. the activation, a float scalar value.
    '''
    dL_da = (compute_L(a + delta, y) - compute_L(a, y)) / delta
    return dL_da

# --------------------------
def check_da_dz(z, delta=1e-7):
    '''
        Compute local gradient of the sigmoid function using gradient check.
        Input:
            z: the logit value of logistic regression, a float scalar.
            delta: a small number for gradient check, a float scalar.
        Output:
            da_dz: the approximated local gradient of activation a w.r.t. the logit z, a float scalar value.
    '''
    da_dz = (compute_a(z + delta) - compute_a(z)) / delta
    return da_dz

# --------------------------
def check_dz_dw(x, w, b, delta=1e-7):
    '''
        compute the partial gradients of the logit function z w.r.t. weights w using gradient checking.
        The idea is to add a small number to the weights and b separately, and approximate the true gradient using numerical gradient.
        Input:
            x: the feature vector of a data instance, a float numpy vector of length p. Here p is the number of features/dimensions.
            w: the weight vector of the logistic model, a float numpy vector of length p.
            b: the bias value of the logistic model, a float scalar.
            delta: a small number for gradient check, a float scalar.
        Output:
            dz_dw: the approximated partial gradient of logit z w.r.t. the weight vector w computed using gradient check, a numpy float vector of length p.
    '''
    p = x.shape[0]
    dz_dw = np.mat(np.zeros(p)).T
    for i in range(p):
        d = np.mat(np.zeros(p)).T
        d[i] = delta
        dz_dw[i] = (compute_z(x, w + d, b) - compute_z(x, w, b)) / delta
    return dz_dw

# --------------------------
def check_dz_db(x, w, b, delta=1e-7):
    '''
        compute the partial gradients of the logit function z w.r.t. the bias b using gradient checking.
        The idea is to add a small number to the weights and b separately, and approximate the true gradient using numerical gradient.
        For example, the true gradient of logit z w.r.t. bias can be approximated as  [z(w,b+ delta) - z(w,b)] / delta , here delta is a small number.
        Input:
            x: the feature vector of a data instance, a float numpy vector of length p. Here p is the number of features/dimensions.
            w: the weight vector of the logistic model, a float numpy vector of length p.
            b: the bias value of the logistic model, a float scalar.
            delta: a small number for gradient check, a float scalar.
        Output:
            dz_dw: the approximated partial gradient of logit z w.r.t. the weight vector w computed using gradient check, a numpy float vector of length p.
            dz_db: the approximated partial gradient of logit z w.r.t. the bias b using gradient check, a float scalar.
    '''
    dz_db = (compute_z(x, w, b + delta) - compute_z(x, w, b)) / delta
    return dz_db

# --------------------------
def check_dL_dw(x, y, w, b, delta=1e-7):
    '''
       Given an instance in the training data, compute the gradient of the weights w using gradient check.
        Input:
            x: the feature vector of a training instance, a float numpy vector of length p. Here p is the number of features/dimensions.
            y: the label of a training instance, an integer scalar value. The values can be 0 or 1.
            w: the weight vector, a float numpy vector of length p.
            b: the bias value, a float scalar.
            delta: a small number for gradient check, a float scalar.
        Output:
            dL_dw: the approximated gradient of the loss function w.r.t. the weight vector, a numpy float vector of length p.
    '''
    p = x.shape[0]  # number of features
    dL_dw = np.mat(np.zeros(p)).T
    for i in range(p):
        d = np.mat(np.zeros(p)).T
        d[i] = delta
        dL_dw[i] = (forward(x, y, w + d, b)[-1] - forward(x, y, w, b)[-1]) / delta
    return dL_dw

# --------------------------
def check_dL_db(x, y, w, b, delta=1e-7):
    '''
       Given an instance in the training data, compute the gradient of the bias b using gradient check.
        Input:
            x: the feature vector of a training instance, a float numpy vector of length p. Here p is the number of features/dimensions.
            y: the label of a training instance, an integer scalar value. The values can be 0 or 1.
            w: the weight vector, a float numpy vector of length p.
            b: the bias value, a float scalar.
            delta: a small number for gradient check, a float scalar.
        Output:
            dL_db: the approximated gradient of the loss function w.r.t. the bias, a float scalar.
    '''
    dL_db = (forward(x, y, w, b + delta)[-1] - forward(x, y, w, b)[-1]) / delta
    return dL_db


# %%
"""We wanna get familiar with array manipulation(File 17-21)."""
import numpy as np
# %%
help(np.concatenate)
# %%
a = np.arange(1, 5)
b = np.arange(6)
c = np.arange(4)
# %%
np.concatenate((a, b))
# %%
np.concatenate((a, b, c))
# %%
c = np.zeros(10)
# %%
np.concatenate((a, b), out=c)
# %%
c = np.concatenate((a, b))
c
# %%
"""Both should be the same dimension."""
a = np.arange(1, 5).reshape(2, 2)
b = np.arange(5, 7).reshape(1, 2)
# %%
np.concatenate((a, b))
# %%
"""
np.concatenate((a, b), axis=1) will raise an error.

ValueError:
all the input array dimensions for the concatenation axis
must match exactly, but along dimension 0, the array at index 0
has size 2 and the array at index 1 has size 1
"""
np.concatenate((a, b.T), axis=1)
# %%
a = np.arange(5)
b = np.arange(1, 5).reshape(2, 2)
"""np.concatenate((a, b)) will raise an error."""
# %%
"""vstack vertical stack"""
help(np.vstack)
# %%
a = np.arange(6)
n = np.arange(5)
c = np.arange(5)+1
# %%
"""np.vstack((a, n)) will raise an error."""
np.vstack((n, c))
# %%
np.vstack((c, n))
# %%
a = np.arange(1, 5).reshape(2, 2)
b = np.arange(5, 7).reshape(1, 2)
# %%
np.concatenate((a, b))
# %%
np.vstack((a, b))
# %%
"""hstack horizontal stack"""
help(np.hstack)
# %%
a = np.arange(6)
n = np.arange(5)
c = np.arange(5)+1
# %%
# %%
np.hstack((a, n))
# %%
a = np.arange(1, 5).reshape(2, 2)
b = np.arange(5, 7).reshape(1, 2)
# %%
np.hstack((a, b.T))
# %%
help(np.split)
# %%
a = np.arange(1, 10)
# %%
"""np.split(a, 4) will raise an error."""
np.split(a, 3)
# %%
a = np.arange(1, 13).reshape(6, 2)
a
# %%
np.split(a, 2)
# %%
help(np.hsplit)
# %%
help(np.vsplit)
# %%
np.vsplit(a, 2)
# %%
a = np.arange(1, 13).reshape(2, 6)
a
# %%
np.hsplit(a, 3)
# %%
np.split(a, 3, axis=1)
# %%


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

import numpy as np
import csv
import sys
import matplotlib.pyplot as plt

class gradient_descent_algorithm:
    def __init__(self,x,y,alpha,total_iteration):
        self.x = x    # x represents input features of each training examples. x is a nxm matrix.
        self.y = y    # y represents outputs of each training examples. y is a real number.
        self.alpha = alpha
        self.total_iteration = total_iteration

    def get_cost_function(self,theta):
        m = self.x.shape[1]    # m represents number of training examples. Amount of columns in matrix x is passed to m.
        squared_error = 0
        squared_error_sum = 0
        for mm in range(0,m):
            exponent_of_base_e = -(theta.transpose() * self.x[:,mm])
            exponential_function = np.exp(exponent_of_base_e[0,0])
            hypothesis = 1 / (1 + exponential_function)
            squared_error = self.y[0,mm] * np.log(hypothesis) + (1 - self.y[0,mm]) * np.log(1 - hypothesis)
            squared_error_sum += squared_error
        cost_function = -(squared_error_sum / m)
        return cost_function

    def gradient_descent(self):
        n = self.x.shape[0]    # n represents number of features. Amount of rows in matrix x is passed to n.
        m = self.x.shape[1]    # m represents number of training examples. Amount of columns in matrix x is passed to m.
        iteration_count = 0
        cost_function   = 0
        theta = np.matrix([[None]]*n)    # theta represents parameters of x of each training examples. theta is a nx1 matrix.
        for nn in range(0,n):
            theta[nn,0] = 0
        cost_function_vs_iteration_count_plot = plt.subplot(212)
        cost_function_vs_iteration_count_plot.set_xlabel('Iterations of gradient descent')
        cost_function_vs_iteration_count_plot.set_ylabel(r'Cost function J($\Theta$)')
        for iteration in range(self.total_iteration):
            cost_function = self.get_cost_function(theta)
            partial_derivative_of_squared_error_sum = np.matrix([[None]]*n)
            for nn in range(0,n):
                partial_derivative_of_squared_error_sum[nn,0] = 0
            for mm in range(0,m):
                exponent_of_base_e = -(theta.transpose() * self.x[:,mm])
                exponential_function = np.exp(exponent_of_base_e[0,0])
                hypothesis = 1 / (1 + exponential_function)
                cost = hypothesis - self.y[0,mm]
                partial_derivative_of_squared_error = self.x[:,mm] * cost * 2
                partial_derivative_of_squared_error_sum += partial_derivative_of_squared_error
            partial_derivative_of_cost_function = partial_derivative_of_squared_error_sum / (2 * m)
            theta = theta - partial_derivative_of_cost_function * self.alpha
            iteration_count += 1
            print '========================================'
            print 'iteration_count = ' , iteration_count
            print 'cost_function = '   , cost_function
            print 'theta = '           , theta
            cost_function_vs_iteration_count_plot.plot(iteration_count, cost_function, color='black', marker='o', markersize=2)
        print '========================================'
        print 'Learning rate = ', self.alpha
        print 'Final theta = ', theta
        print '========================================'
        return theta

class read_csv_file:
    def __init__(self,csv_file_name):
        self.csv_file_name = csv_file_name

    def read_csv_file_method(self):
        with open(self.csv_file_name) as csvfile:
            data_classification_list = []
            lines = csv.reader(csvfile, delimiter=',')
            number_of_lines = 0
            for line in lines:
                line_list = [1]
                for line_length in range(0,len(line)):
                    line_list.append(float(line[line_length]))
                data_classification_list.append(line_list)
                line_length = len(line_list)
                number_of_lines = number_of_lines + 1
        return number_of_lines, line_length, data_classification_list

class logistic_random_number_generator:
    def __init__(self,number_of_training_examples,number_of_features,data_classification_list):
        self.number_of_training_examples = number_of_training_examples
        self.number_of_features = number_of_features
        self.data_classification_list = data_classification_list

    def logistic_random_number_generator_method(self):
        x = np.matrix([[None]*self.number_of_training_examples]*self.number_of_features)
        y = np.matrix([[None]*self.number_of_training_examples])
        for training_example in range(0,self.number_of_training_examples):
            y[0,training_example] = self.data_classification_list[training_example][line_length-1]
            for feature in range(0,self.number_of_features):
                x[feature,training_example] = self.data_classification_list[training_example][feature]
        return x, y

class plot_hypothesis_and_logistic_random_number:
    def __init__(self,x,y,theta):
        self.x = x
        self.y = y
        self.theta = theta

    def plot_hypothesis_and_logistic_random_number_method(self):
        number_of_training_examples = self.x.shape[1]
        plot_generator = plt.subplot(211)
        plot_generator.set_xlabel('Input x1 of training examples')
        plot_generator.set_ylabel('Input x2 of training examples')
        for training_example in range(0,number_of_training_examples):
            if self.y[0,training_example] == 1:
                plot_generator.plot(self.x[1,training_example], self.x[2,training_example], color='black', marker='x')
            else:
                plot_generator.plot(self.x[1,training_example], self.x[2,training_example], color='black', marker='o')
        theta_2 = self.theta[2,0]
        theta_1 = self.theta[1,0]
        theta_0 = self.theta[0,0]
        x1_axis = np.arange(0.,10.,0.02)
        x2_axis = -((theta_0 + theta_1 * x1_axis) / theta_2)  # theta_2 * x2_axis + theta_1 * x1_axis + theta_0 * 1 = 0 is the Decision Boundary.
        plot_generator.plot(x1_axis, x2_axis, color='black', linestyle='solid', linewidth=2)

# Main function
plt.figure()
input_csv_file = sys.argv[1]
R = read_csv_file(input_csv_file)
number_of_lines, line_length, data_classification_list = R.read_csv_file_method()
L = logistic_random_number_generator(number_of_training_examples=number_of_lines, number_of_features=line_length-1, data_classification_list=data_classification_list)
input_x, output_y = L.logistic_random_number_generator_method()
G = gradient_descent_algorithm(x=input_x, y=output_y, alpha=0.005, total_iteration=50000)
final_theta = G.gradient_descent()
P = plot_hypothesis_and_logistic_random_number(x=input_x, y=output_y, theta=final_theta)
P.plot_hypothesis_and_logistic_random_number_method()
plt.show()


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

import numpy as np

def load_data(file_path):
    data = []
    labels = []
    f = open(file_path, 'r')
    for line in f.readlines():
        line = line.strip("\n")
        if not line:
            continue
        line_list= line.split()
        data.append([float(a) for a in line_list[:-1]])
        labels.append(int(line[-1]))
    return data, labels

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

def train(x, y, steps=100, rate=0.1):
    m, n = x.shape
    y= y.reshape(m, 1)
    w = np.ones((n, 1))
    b = np.ones(1)
    for s in range(steps):
        z = np.dot(x, w) + b
        h = sigmoid(z)
        error = (y - h)
        w = w + (rate * np.dot(x.transpose(), error))/n
        b = b + rate * np.sum(error, axis=0)/n
    return w, b

def predict(w, b, test_vec):
    z = np.dot(test_vec, w) + b
    h = sigmoid(z)
    if h > 0.5 :
        return 1
    else :
        return 0

data, labels = load_data("testSet.txt")
x = np.array(data)
y = np.array(labels)
w, b = train(x, y, 500, 0.001)
test_vec = np.array([0.9, 1.3])
pre = predict(w, b, test_vec)
print(pre)


import matplotlib.pyplot as plt
import numpy as np
import scipy.optimize

T = 2*np.pi
lenght = 1000
t = np.linspace(-2*T, 2*T, lenght)
n = np.array([1, 3, 5, 9, 49, 99, 499, 999, 4999, 9999])+1

fig = plt.figure()
for i in range (0, 10, 1) :
    quadra = np.zeros(lenght)
    for k in range(1, n[i], 2) :
        quadra += 2/(k*np.pi) * np.sin(k*t)
    ax1 = fig.add_subplot(5, 2, i+1)
    ax1.plot(t/T, quadra, label="n="+str(n[i]))
    ax1.legend()
fig.text(0.5, 0.05, 'Periodo [T]', ha='center', va='center')
fig.text(0.1, 0.5, 'Segnale simulato [arb.un.]', ha='center', va='center', rotation='vertical')
plt.show()

fig = plt.figure()
for i in range(0, 6, 1) :
    triang = np.zeros(lenght)
    for k in range(1, n[i], 2) :
        triang += 4/((k*np.pi)**2) * np.cos(k*t)
    ax1 = fig.add_subplot(3, 2, i+1)
    ax1.plot(t/T, triang, label="n="+str(n[i]))
    ax1.legend()
fig.text(0.5, 0.05, 'Periodo [T]', ha='center', va='center')
fig.text(0.1, 0.5, 'Segnale simulato [arb.un.]', ha='center', va='center', rotation='vertical')
plt.show()

Lab 2/Esercizio python Fourier/quadra e triangolare.py


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

rc("text", usetex = True)
R_earth = 6371
h_ion = 350
d = 10
#sample comment

def map3(el):
	f = R_earth/(R_earth + h_ion)
	el = np.deg2rad(el)
	map_function = 1/np.sqrt(1 - (f*np.cos(el))**2)
	return map_function

def map2(el):
	R = R_earth + h_ion
	p = 90 - el
	z = np.deg2rad(p)
	map_function = 1/np.cos(z) + (np.cos(z)**2 - 1)*d**2/(8*R**2*np.cos(z)**5)
	return map_function

def map1(el):
	el = np.deg2rad(el)
	map_function = 1/np.sin(el)
	return map_function

def map4(el):
	a0 = 64.4297
	a1 = 0.0942437
	a2 = 1.39436
	a3 = 19.6357
	b0 = 64.3659
	b1 = 0.104974
	b2 = 1.41152
	b3 = -0.0463341
	z = 90 - el
	z_rad = np.deg2rad(z)
	za = (z - a0)*a1
	za = np.deg2rad(za)
	zb = (z - b0)*b1
	zb = np.deg2rad(zb)
	a = (-np.arctan(za) - a2)*a3
	b = (-np.arctan(zb) - b2)*b3
	p = a + b*d
	map_function = 1/((1 - p/100)*np.cos(z_rad))
	return map_function


import numpy as np
np.random.seed(42)

class ELM(object):

  def __init__(self, inputSize, outputSize, hiddenSize):
    """
    Initialize weight and bias between input layer and hidden layer
    Parameters:
    inputSize: int
        The number of input layer dimensions or features in the training data
    outputSize: int
        The number of output layer dimensions
    hiddenSize: int
        The number of hidden layer dimensions
    """

    self.inputSize = inputSize
    self.outputSize = outputSize
    self.hiddenSize = hiddenSize

    # Initialize random weight with range [-0.5, 0.5]
    self.weight = np.matrix(np.random.uniform(-0.5, 0.5, (self.hiddenSize, self.inputSize)))

    # Initialize random bias with range [0, 1]
    self.bias = np.matrix(np.random.uniform(0, 1, (1, self.hiddenSize)))

    self.H = 0
    self.beta = 0

  def sigmoid(self, x):
    """
    Sigmoid activation function

    Parameters:
    x: array-like or matrix
        The value that the activation output will look for
    Returns:
        The results of activation using sigmoid function
    """
    return 1 / (1 + np.exp(-1 * x))

  def predict(self, X):
    """
    Predict the results of the training process using test data
    Parameters:
    X: array-like or matrix
        Test data that will be used to determine output using ELM
    Returns:
        Predicted results or outputs from test data
    """
    X = np.matrix(X)
    y = self.sigmoid((X * self.weight.T) + self.bias) * self.beta

    return y

  def train(self, X, y):
    """
    Extreme Learning Machine training process
    Parameters:
    X: array-like or matrix
        Training data that contains the value of each feature
    y: array-like or matrix
        Training data that contains the value of the target (class)
    Returns:
        The results of the training process
    """

    X = np.matrix(X)
    y = np.matrix(y)

    # Calculate hidden layer output matrix (Hinit)
    self.H = (X * self.weight.T) + self.bias

    # Sigmoid activation function
    self.H = self.sigmoid(self.H)

    # Calculate the Moore-Penrose pseudoinverse matriks
    H_moore_penrose = np.linalg.inv(self.H.T * self.H) * self.H.T

    # Calculate the output weight matrix beta
    self.beta = H_moore_penrose * y

    return self.H * self.beta


import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from scipy.optimize import curve_fit

def T(LD):
    return 2 * ((LD) ** 0.5)

def radian(degree):
    return degree * np.pi / 180

def chi_square(ys, fs, sigma):
    chi_sqr = 0
    for i in range(0, len(ys)):
        chi_sqr += ((ys[i] - fs[i]) / sigma[i]) ** 2
    return chi_sqr

def fit(t, tau):
    return rad[0] * np.exp(-t / tau) * np.cos(2 * np.pi * (t / period))

data = pd.read_csv(r"C:\Users\Walter\IdeaProjects\PHY224\pendulum\Data1.csv", delimiter=',', skiprows=0)
t = data['t'] - data['t'].iloc[0]  # set the beginning of timeline to 0
x = data['x']
y = data['y']
rad = data['rad']
err = radian(np.zeros(len(y)) + 0.5)
period = T(0.35)
Terr = T(0.36) - period
print(Terr)
print("Period from formula: " + str(period))
print("Period observed: " + str(19 / 24))

popt, pcov = curve_fit(fit, t, rad, 10, err, True)
cov2 = np.diag(pcov)
plt.errorbar(t, rad, yerr=err, xerr=0.1, label='Original data')
plt.plot(t, fit(t, *popt), color='red', marker='|',
         label='fit: tau=%.3e +- ' % tuple(popt) + '%.3e' % tuple(np.sqrt(cov2)))
plt.xlabel("Time (s)")
plt.ylabel("Angle (rad)")
plt.title('Figure.1 ')
plt.legend(loc='best')
plt.show()
print(popt, ' +- ', np.sqrt(cov2))
ys = np.zeros(len(rad))
for i in range(0, len(rad)):
    ys[i] = fit(t[i], *popt)
v = len(t) - len(popt)
r2 = chi_square(ys, rad, err)
print(r2 / v)

data2 = pd.read_csv(r"C:\Users\Walter\IdeaProjects\PHY224\pendulum\Data2.csv", delimiter=',', skiprows=1)
t = data2['t'] - data2['t'].iloc[0]  # set the beginning of timeline to 0
x = data2['x']
y = data2['y']
rad = data2['rad']
err = radian(np.zeros(len(y)) + 0.5)
period = T(0.35)
Terr = T(0.36) - period
print(Terr)
print("Period from formula: " + str(period))
print("Period observed: " + str(28 / 35))

popt, pcov = curve_fit(fit, t, rad, 10, err, True)
cov2 = np.diag(pcov)
plt.errorbar(t, rad, yerr=err, xerr=0.1, label='Original data')
plt.plot(t, fit(t, *popt), color='red', marker='|',
         label='fit: tau=%.3e +- ' % tuple(popt) + '%.3e' % tuple(np.sqrt(cov2)))
plt.xlabel("Time (s)")
plt.ylabel("Angle (rad)")
plt.title('Figure.2 ')
plt.legend(loc='best')
plt.show()
print(popt, ' +- ', np.sqrt(cov2))
ys = np.zeros(len(rad))
for i in range(0, len(rad)):
    ys[i] = fit(t[i], *popt)
v = len(t) - len(popt)
r2 = chi_square(ys, rad, err)
print(r2 / v)

data3 = pd.read_csv(r"C:\Users\Walter\IdeaProjects\PHY224\pendulum\Data3.csv", delimiter=',', skiprows=0)
t = data3['t'] - data3['t'].iloc[0]  # set the beginning of timeline to 0
x = data3['x']
y = data3['y']
rad = data3['rad']
err = radian(np.zeros(len(y)) + 0.5)
period = T(0.35)
Terr = T(0.36) - period
print(Terr)
print("Period from formula: " + str(period))
print("Period observed: " + str(30 / 37))

popt, pcov = curve_fit(fit, t, rad, 10, err, True)
cov2 = np.diag(pcov)
plt.errorbar(t, rad, yerr=err, xerr=0.1, label='Original data')
plt.plot(t, fit(t, *popt), color='red', marker='|',
         label='fit: tau=%.3e +- ' % tuple(popt) + '%.3e' % tuple(np.sqrt(cov2)))
plt.xlabel("Time (s)")
plt.ylabel("Angle (rad)")
plt.title('Figure.3 ')
plt.legend(loc='best')
plt.show()
print(popt, ' +- ', np.sqrt(cov2))
ys = np.zeros(len(rad))
for i in range(0, len(rad)):
    ys[i] = fit(t[i], *popt)
v = len(t) - len(popt)
r2 = chi_square(ys, rad, err)
print(r2 / v)

data4 = pd.read_csv(r"C:\Users\Walter\IdeaProjects\PHY224\pendulum\Data4.csv", delimiter=',', skiprows=0)
t = data4['t'] - data4['t'].iloc[0]  # set the beginning of timeline to 0
x = data4['x']
y = data4['y']
rad = data4['rad']
err = radian(np.zeros(len(y)) + 0.5)
period = T(0.35)
Terr = T(0.36) - period
print(Terr)
print("Period from formula: " + str(period))
print("Period observed: " + str(31 / 40))

popt, pcov = curve_fit(fit, t, rad, 10, err, True)
cov2 = np.diag(pcov)
plt.errorbar(t, rad, yerr=err, xerr=0.1, label='Original data')
plt.plot(t, fit(t, *popt), color='red', marker='|',
         label='fit: tau=%.3e +- ' % tuple(popt) + '%.3e' % tuple(np.sqrt(cov2)))
plt.xlabel("Time (s)")
plt.ylabel("Angle (rad)")
plt.title('Figure.4 ')
plt.legend(loc='best')
plt.show()
print(popt, ' +- ', np.sqrt(cov2))
ys = np.zeros(len(rad))
for i in range(0, len(rad)):
    ys[i] = fit(t[i], *popt)
v = len(t) - len(popt)
r2 = chi_square(ys, rad, err)
print(r2 / v)


"""
Implementation of the Chinese Remainder Theorem.

The code in this file is taken from SymPy (see the license below). We could
(and used to) just import sympy.ntheory.modular.crt, but SymPy is a very heavy
dependency for a rather basic algorithm.

Note: although many of the helper functions from SymPy are copied here, these
functions should not be used by code outside of ndindex. If you want to use
one of these algorithms, you should import them from SymPy (note that the
functions here have had their functionality stripped down from the SymPy
versions).

License
-------

Copyright (c) 2006-2021 SymPy Development Team

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

  a. Redistributions of source code must retain the above copyright notice,
     this list of conditions and the following disclaimer.
  b. Redistributions in binary form must reproduce the above copyright
     notice, this list of conditions and the following disclaimer in the
     documentation and/or other materials provided with the distribution.
  c. Neither the name of SymPy nor the names of its contributors
     may be used to endorse or promote products derived from this software
     without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
"""

from functools import reduce
from operator import mul
from math import gcd

def gcdex(a, b):
    """Returns x, y, g such that g = x*a + y*b = gcd(a, b).

    Examples
    ========

    >>> from ndindex._crt import gcdex
    >>> gcdex(2, 3)
    (-1, 1, 1)
    >>> gcdex(10, 12)
    (-1, 1, 2)

    >>> x, y, g = gcdex(100, 2004)
    >>> x, y, g
    (-20, 1, 4)
    >>> x*100 + y*2004
    4

    """
    if (not a) and (not b):
        return (0, 1, 0)

    if not a:
        return (0, b//abs(b), abs(b))
    if not b:
        return (a//abs(a), 0, abs(a))

    if a < 0:
        a, x_sign = -a, -1
    else:
        x_sign = 1

    if b < 0:
        b, y_sign = -b, -1
    else:
        y_sign = 1

    x, y, r, s = 1, 0, 0, 1

    while b:
        (c, q) = (a % b, a // b)
        (a, b, r, s, x, y) = (b, c, x - q*r, y - q*s, r, s)

    return (x*x_sign, y*y_sign, a)

# np.prod has overflow and math.prod is Python 3.8+ only
def prod(seq):
    return reduce(mul, seq, 1)

def _crt(U, M):
    """
    Chinese Remainder Theorem.

    Given a set of integer residues ``u_0,...,u_n`` and a set of
    co-prime integer moduli ``m_0,...,m_n``, returns an integer
    ``u``, such that ``u = u_i mod m_i`` for ``i = ``0,...,n``.

    Examples
    ========

    Consider a set of residues ``U = [49, 76, 65]``
    and a set of moduli ``M = [99, 97, 95]``. Then we have::

       >>> from ndindex._crt import _crt

       >>> _crt([49, 76, 65], [99, 97, 95])
       639985

    This is the correct result because::

       >>> [639985 % m for m in [99, 97, 95]]
       [49, 76, 65]

    Note: this is a low-level routine with no error checking.
    """
    p = prod(M)
    v = 0

    for u, m in zip(U, M):
        e = p // m
        s, _, _ = gcdex(e, m)
        v += e*(u*s % m)

    return v % p

def solve_congruence(*remainder_modulus_pairs):
    """Compute the integer ``n`` that has the residual ``ai`` when it is
    divided by ``mi`` where the ``ai`` and ``mi`` are given as pairs to
    this function: ((a1, m1), (a2, m2), ...). If there is no solution,
    return None. Otherwise return ``n`` and its modulus.

    The ``mi`` values need not be co-prime.

    Examples
    ========

    >>> from ndindex._crt import solve_congruence

    What number is 2 mod 3, 3 mod 5 and 2 mod 7?

    >>> solve_congruence((2, 3), (3, 5), (2, 7))
    23
    >>> [23 % m for m in [3, 5, 7]]
    [2, 3, 2]

    If you prefer to work with all remainder in one list and
    all moduli in another, send the arguments like this:

    >>> solve_congruence(*zip((2, 3, 2), (3, 5, 7)))
    23

    The moduli need not be co-prime; in this case there may or
    may not be a solution:

    >>> solve_congruence((2, 3), (4, 6)) is None
    True

    >>> solve_congruence((2, 3), (5, 6))
    5

    """
    def combine(c1, c2):
        """Return the tuple (a, m) which satisfies the requirement
        that n = a + i*m satisfy n = a1 + j*m1 and n = a2 = k*m2.

        References
        ==========

        .. [1] https://en.wikipedia.org/wiki/Method_of_successive_substitution
        """
        a1, m1 = c1
        a2, m2 = c2
        a, b, c = m1, a2 - a1, m2
        g = reduce(gcd, [a, b, c])
        a, b, c = [i//g for i in [a, b, c]]
        if a != 1:
            inv_a, _, g = gcdex(a, c)
            if g != 1:
                return None
            b *= inv_a
        a, m = a1 + m1*b, m1*c
        return a, m

    rm = remainder_modulus_pairs

    rv = (0, 1)
    for rmi in rm:
        rv = combine(rv, rmi)
        if rv is None:
            break
        n, m = rv
        n = n % m
    else:
        return n

def crt(m, v, check=True):
    r"""Chinese Remainder Theorem.

    The moduli in m are assumed to be pairwise coprime.  The output
    is then an integer f, such that f = v_i mod m_i for each pair out
    of v and m.

    If the moduli are not co-prime the correct result will be returned
    if/when the test of the result is found to be incorrect. This result
    will be None if there is no solution.

    The keyword ``check`` can be set to False if it is known that the moduli
    are coprime.

    Examples
    ========

    As an example consider a set of residues ``U = [49, 76, 65]``
    and a set of moduli ``M = [99, 97, 95]``. Then we have::

       >>> from ndindex._crt import crt

       >>> crt([99, 97, 95], [49, 76, 65])
       639985

    This is the correct result because::

       >>> [639985 % m for m in [99, 97, 95]]
       [49, 76, 65]

    If the moduli are not co-prime, you may receive an incorrect result
    if you use ``check=False``:

       >>> crt([12, 6, 17], [3, 4, 2], check=False)
       954
       >>> [954 % m for m in [12, 6, 17]]
       [6, 0, 2]
       >>> crt([12, 6, 17], [3, 4, 2]) is None
       True
       >>> crt([3, 6], [2, 5])
       5

    Note: the order of gf_crt's arguments is reversed relative to crt,
    and that solve_congruence takes residue, modulus pairs.

    Programmer's note: rather than checking that all pairs of moduli share
    no GCD (an O(n**2) test) and rather than factoring all moduli and seeing
    that there is no factor in common, a check that the result gives the
    indicated residuals is performed -- an O(n) operation.
    """
    result = _crt(v, m)

    if check:
        if not all(v % m == result % m for v, m in zip(v, m)):
            result = solve_congruence(*list(zip(v, m)))

    return result

def ilcm(a, b):
    """Computes integer least common multiple.

    Examples
    ========

    >>> from ndindex._crt import ilcm
    >>> ilcm(5, 10)
    10
    >>> ilcm(7, 3)
    21

    """
    if 0 in [a, b]:
        return 0
    return a // gcd(a, b) * b # since gcd(a,b) | a


import numpy as np

def cross_entropy(Y, P):
    # two class cross entropy
    # Write a function that takes as input two lists Y, P,
    # and returns the float corresponding to their cross-entropy.
    # Y is the true labels and P is the predicted probabilities.
    Y = np.float_(Y)
    P = np.float_(P)
    return -np.sum(Y * np.log(P) + (1 - Y) * np.log(1 - P))

# Reference:-
# https://datascience.stackexchange.com/questions/9302/the-cross-entropy-error-function-in-neural-networks


import numpy as np

class BinaryCrossentropy:
    def __init__(self):
        self.epsilon = 1e-15

    def __call__(self, y: np.ndarray, y_pred: np.ndarray) -> np.float64:
        return self.loss(y, y_pred)

    def loss(self, y: np.ndarray, y_pred: np.ndarray) -> np.float64:
        # Avoid division by zero
        y_pred = np.clip(y_pred, self.epsilon, 1 - self.epsilon)
        return - y * np.log(y_pred) - (1 - y) * np.log(1 - y_pred)

    def gradient(self, y: np.ndarray, y_pred: np.ndarray) -> np.float64:
        # Avoid division by zero
        y_pred = np.clip(y_pred, self.epsilon, 1 - self.epsilon)
        return - (y / y_pred) + (1 - y) / (1 - y_pred)


# Code to compute wilsonscore confidence interval
from scipy.special import ndtri
import numpy as np
def rb_wilsonscore(count,nobs,confint):
	#-----------------------------------------------------------------------------------
	# This function computes the wilson score confidence intervals Score Interval for a binomial distribution.
	#
	#     Input :     count   =    Number of successes
	#                 nobs    =    Number of total Trials
	#                 confint =    Confindence interval for which Wilson Score is computed [e.g. confint =0.95 2\sigma]
	#
	#     Output
	#				  center =  gives the center of the score intervals given the data
	#                 hi     =   Upper bound for given confint
	#                 lo     =   Lower bound for given confint
	#
	#     Example:    import rb_wilsonscore as w
	#                 XC, hi, lo = w.rb_wilsonscore(10.,20.,.95)
	#
	#     Written by :   Rongmon Bordoloi  Nov 2017
	#     Tested on  : Python 2.7, 3.x
	#-----------------------------------------------------------------------------------
	count=np.double(count)
	nobs=np.double(nobs)
	confint=np.double(confint)
	# Written by RB
	if nobs == 0.0: return (0.0,0.5 ,1.0)
	z = ndtri(1. - 0.5 * (1.-confint))
	p=count/nobs
	# now do it with Wilson score interval
	alpha=(p+ (z*z)/(2.*nobs))
	beta=((p*(1.-p)/ nobs) + ((z**2.)/(4.*(nobs**2.))))**0.5
	center = (alpha) / (1. + ((z**2.)/ nobs))
	hi = (alpha+ (z*beta))/ (1. + ((z**2.)/ nobs))
	lo = (alpha - z*(beta)) / (1. + ((z**2.)/ nobs))
	return (center,hi,lo)


"""Solution for up-and-in option with control Monte-Carlo"""
import numpy as np
from scipy import stats

################################################################################
# Simulation parameters
M = 10000
r = 0.02
sg = 0.3
dt = 1 / 252
T = 1
n = int(T / dt)
S0 = 1
K = 0.9
################################################################################

# Generate random numbers from a normal distribution for the MC simulation
eps = np.random.normal(size=(n, M))
f_avg = np.zeros((M))
f_geom = np.zeros((M))
sgg = sg / np.sqrt(3)
a = 0.5 * (r - 0.5 * sgg ** 2)
d1 = (np.log(S0 / K) + (a + 0.5 * sgg ** 2) * T) / (sgg * np.sqrt(T))
d2 = d1 - sgg * np.sqrt(T)
c_geom = S0 * np.exp((a - r) * T) * stats.norm.cdf(d1) - \
    K * np.exp(-r * T) * stats.norm.cdf(d2)

# Iterate through M iterations
for i in range(M):
    S = np.zeros((n))
    S[0] = S0
    # Iterate through n - 1 time steps (first time step is known)
    for j in range(n - 1):
        # normal pricing is stored in S
        S[j + 1] = S[j] * np.exp((r - 0.5 * sg ** 2) * dt \
             + sg * eps[j, i] * np.sqrt(dt))
    # Find the average value of the realized stock price path
    S_avg = np.mean(S)
    # Find also the MC geometric average
    S_geom = stats.mstats.gmean(S)

    # Discounted payoffs
    f_avg[i] = np.exp(-r * n * dt) * max(S_avg - K, 0)
    f_geom[i] = np.exp(-r * n * dt) * max(S_geom - K, 0)

# Calculate the optimal step size
b = np.cov(f_geom, f_avg)[0, 1] / np.var(f_geom)

# Calculate the optimal option price
c_avg = np.mean(f_avg) - b * (np.mean(f_geom) - c_geom)

# Print the results
print('Price, (c_geom)', c_geom)
print('Price (mean), control pricing:', np.mean(c_avg))
print('Price (mean), normal pricing:', np.mean(f_avg))
print('STD, control pricing:', np.std(f_avg))
print('STD, norm pricing:', np.std(f_avg))

# It can be seen that antithetic pricing has a lower standard deviation.
# This is because of the use of antithetic variates.


"""Utility functions for weights."""

from typing import Tuple

import numpy as np

def initialise_weights(size: Tuple, method: str='gauss') -> np.ndarray:
    """Randomly initialise a weight matrix of a given size

    Parameters
    ----------
    size : tuple of int
        The input x output size of the weight matrix
    method : {'gauss', 'xavier', 'xavier-average', 'he'}
        The initialization method,
        if none given a values will be drawn from a Uniform(-1, 1)
        - gauss : drawn from a Normal(0, 1)
        - xavier : drawn from a Normal(0, 1 / N_in)
        - xavier-average : drawn from a Normal(0, 2 / (N_in + N_out))
        - he : drawn from a Normal(0, 2/N_in)

    Returns
    -------
    weights : ndarray
        The weight matrix

    """
    if method == 'gauss':
        return np.random.normal(0, 1, size)
    elif method == 'xavier':
        std_dev = np.sqrt(1/size[0])
        return np.random.normal(0, std_dev, size)
    elif method == 'xavier-average':
        std_dev = 2/(size[0] + size[1])
        return np.random.normal(0, std_dev, size)
    elif method == 'he':
        std_dev = 2/size[0]
        return np.random.normal(0, std_dev, size)
    else:
        return np.random.uniform(-1, 1, size)


import os
import numpy as np
import pandas as pd

from consolemenu import Screen

from app.utils.data_imports import *
from app.utils.mathPlots import *
from app.utils.mathLib import *

def action_minimum_squares(X, Y):
    np.set_printoptions(precision=2)
    res = minimum_squares(X, Y)

    print("\n")
    print('Dado, Y= mX + c')
    print('\nY= {:0.2f}X + ({:0.2f}): '.format(res["m"],res["c"]) )
    print('m: {:0.2f} kg'.format(res["m"]))
    print('c: {:0.2f}'.format(res["c"]))

    print('\nreta:\n{} '.format(res["reta"].values))

    print('\nmedia_X: \n{}'.format(res["media_X"]))
    print('\nmedia_Y: \n{}'.format(res["media_Y"]))
    print('\nerro_x: \n{}'.format(res["erro_x"].values))
    print('\nerro_y: \n{}'.format(res["erro_y"].values))
    print('\nsoma_erro_xy: \n{}'.format(res["soma_erro_xy"]))
    print('\nerro_x_quadratico: \n{}'.format(res["erro_x_quadratico"].values))
    print('\nsoma_erro_x_quadratico: \n{}'.format(res["soma_erro_x_quadratico"]))
    print("\n")

def action_plu(A):
    res = plu(A)
    print("\nResultado P:")
    print(res["P"])
    print("\nResultado L:")
    print(res["L"])
    print("\nResultado U:")
    print(res["U"])

def action_pvd(A):
    res = pvd(A)
    print("\nResultado U:")
    print(res["U"])
    print("\nResultado s:")
    print(res["s"])
    print("\nResultado V:")
    print(res["V"])

def action_plot(X, Y):
    linear_values = minimum_squares(X, Y)
    plot(X, Y,
    ajust = True,
    line = linear_values["reta"],
    line_label='Ajuste linear',
    line_color='red')

    Screen().input('Press [Enter] to continue')

def app():
  np.set_printoptions(precision=2)

  #get path from SO
  project_path = os.path.dirname(os.path.realpath(__file__))

  # # Import data from pandas
  data = pd.read_csv(project_path+'\\app\\data\\dados_07.csv')

  print("\n\n--------- Algelin II - UFRJ --------- ")
  print("------- Trabalho de analise de Iris -------- ")

  Screen().input('\n\nPress [Enter] to continue')

  print('\nQuestão 01:')
  print('- Método dos mínimos quadrados, Coeficiesntes. 01:')

  print('--- Iris-setosa: ---')
  print('--- Dados de Sepal')
  action_minimum_squares(
    extraction_colunms_value(data, 'Iris-setosa', 'SepalLengthCm'),
    extraction_colunms_value(data, 'Iris-setosa', 'SepalWidthCm'))

  Screen().input('\n\nPress [Enter] to plot example Sepal')

  action_plot(
    extraction_colunms_value(data, 'Iris-setosa', 'SepalLengthCm'),
    extraction_colunms_value(data, 'Iris-setosa', 'SepalWidthCm'))

  print('\n--- Dados de Petal')
  action_minimum_squares(
    extraction_colunms_value(data, 'Iris-setosa', 'PetalLengthCm'),
    extraction_colunms_value(data, 'Iris-setosa', 'PetalWidthCm'))
  print('\n')

  print('--- Iris-versicolor: ---')
  print('--- Dados de Sepal')
  action_minimum_squares(
    extraction_colunms_value(data, 'Iris-versicolor', 'SepalLengthCm'),
    extraction_colunms_value(data, 'Iris-versicolor', 'SepalWidthCm'))

  Screen().input('\n\nPress [Enter] to plot example Petal')

  action_plot(
    extraction_colunms_value(data, 'Iris-versicolor', 'PetalLengthCm'),
    extraction_colunms_value(data, 'Iris-versicolor', 'PetalWidthCm'))
  print('\n--- Dados de Petal')
  action_minimum_squares(
    extraction_colunms_value(data, 'Iris-versicolor', 'PetalLengthCm'),
    extraction_colunms_value(data, 'Iris-versicolor', 'PetalWidthCm'))
  print('\n')

  print('--- Iris-virginica: ---')
  print('--- Dados de Sepal')
  action_minimum_squares(
    extraction_colunms_value(data, 'Iris-virginica', 'SepalLengthCm'),
    extraction_colunms_value(data, 'Iris-versicolor', 'SepalWidthCm'))

  Screen().input('\n\nPress [Enter] to plot example Sepal')

  action_plot(
    extraction_colunms_value(data, 'Iris-virginica', 'SepalLengthCm'),
    extraction_colunms_value(data, 'Iris-versicolor', 'SepalWidthCm'))
  print('\n--- Dados de Petal')
  action_minimum_squares(
    extraction_colunms_value(data, 'Iris-virginica', 'PetalLengthCm'),
    extraction_colunms_value(data, 'Iris-versicolor', 'PetalWidthCm'))
  print('\n')

  print('\n--- Implementação de PLU ---')
  print('--- Iris-setosa ---\n')
  action_plu(constructor_matrix([
    extraction_colunms_value(data, 'Iris-setosa', 'SepalLengthCm'),
    extraction_colunms_value(data, 'Iris-setosa', 'SepalWidthCm'),
    extraction_colunms_value(data, 'Iris-setosa', 'PetalLengthCm'),
    extraction_colunms_value(data, 'Iris-setosa', 'PetalWidthCm')
  ]))
  print('\n')
  Screen().input('\n\nPress [Enter] to continue')

  print('--- Iris-versicolor ---\n')
  action_plu(constructor_matrix([
    extraction_colunms_value(data, 'Iris-versicolor', 'SepalLengthCm'),
    extraction_colunms_value(data, 'Iris-versicolor', 'SepalWidthCm'),
    extraction_colunms_value(data, 'Iris-versicolor', 'PetalLengthCm'),
    extraction_colunms_value(data, 'Iris-versicolor', 'PetalWidthCm')
  ]))
  print('\n')
  Screen().input('\n\nPress [Enter] to continue')

  print('--- Iris-virginica ---\n')
  action_plu(constructor_matrix([
    extraction_colunms_value(data, 'Iris-virginica', 'SepalLengthCm'),
    extraction_colunms_value(data, 'Iris-virginica', 'SepalWidthCm'),
    extraction_colunms_value(data, 'Iris-virginica', 'PetalLengthCm'),
    extraction_colunms_value(data, 'Iris-virginica', 'PetalWidthCm')
  ]))
  print('\n')
  Screen().input('\n\nPress [Enter] to continue')

#bksub

#q02 decespec

#q03
  print('\n- Decomposição em valores singulares (SVD) ')
  print('--- Iris-setosa ---\n')
  action_pvd(constructor_matrix([
    extraction_colunms_value(data, 'Iris-setosa', 'SepalLengthCm'),
    extraction_colunms_value(data, 'Iris-setosa', 'SepalWidthCm'),
    extraction_colunms_value(data, 'Iris-setosa', 'PetalLengthCm'),
    extraction_colunms_value(data, 'Iris-setosa', 'PetalWidthCm')
  ]))
  print('\n')
  Screen().input('\n\nPress [Enter] to continue')

  print('--- Iris-versicolor ---\n')
  action_pvd(constructor_matrix([
    extraction_colunms_value(data, 'Iris-versicolor', 'SepalLengthCm'),
    extraction_colunms_value(data, 'Iris-versicolor', 'SepalWidthCm'),
    extraction_colunms_value(data, 'Iris-versicolor', 'PetalLengthCm'),
    extraction_colunms_value(data, 'Iris-versicolor', 'PetalWidthCm')
  ]))
  print('\n')
  Screen().input('\n\nPress [Enter] to continue')

  print('--- Iris-virginica ---\n')
  action_pvd(constructor_matrix([
    extraction_colunms_value(data, 'Iris-virginica', 'SepalLengthCm'),
    extraction_colunms_value(data, 'Iris-virginica', 'SepalWidthCm'),
    extraction_colunms_value(data, 'Iris-virginica', 'PetalLengthCm'),
    extraction_colunms_value(data, 'Iris-virginica', 'PetalWidthCm')
  ]))
  print('\n')
  Screen().input('\n\nPress [Enter] to continue')

  return

if __name__ == '__main__':
  app()


import numpy as np

from multi_cost import multi_cost

def gradient_descent(x, y, theta, alpha, iterations):
    """
        Performs gradient descent to optimize the 'theta' parameters. Updates theta for a total of
        inputted 'iterations', with a learning rate 'alpha'.

        Parameters
        ----------
        x : array_like
            Shape (m, n+1), where m is the number of examples, and n+1 the number of features
            including the vector of ones for the zeroth parameter.

        y : array_like
            Shape (m, 1), where m is the value of the function at each point.

        theta : array_like
            Shape (1, n+1). The multiple regression parameters.

        alpha : float
            The learning rate.

        iterations : int
            The number of iterations for gradient descent.

        Returns
        -------
        theta : array_like
            Shape (1, n+1). The learned multiple regression parameters.

        cost_history : list
            A list of the values from the cost function after each iteration.
    """

    # Create temporary array for updating theta
    size = y.shape[0]
    parameters = x.shape[1]
    temp = np.zeros((1, parameters))
    cost_history = np.zeros(iterations)

    for iteration in range(iterations):

        error = (1 / size) * ((x @ theta.T) - y)

        for parameter in range(parameters):
            delta = error * x[:, [parameter]]
            temp[0, parameter] = theta[0, parameter] - (alpha * delta.sum())

        theta = temp
        cost_history[iteration] = multi_cost(x, y, theta)

    return theta, cost_history


import numpy as np
import matplotlib.pyplot as plt
np.set_printoptions(threshold=np.nan)
np.set_printoptions(precision=16)

#<startTeX>
# problem discritization
def G(U):
    Gout = np.zeros(m+2)
    for i in range(1,m):
        Gout[i] = eps*(U[i-1]-2*U[i]+U[i+1])/h2+U[i]*((U[i+1]-U[i-1])/(2*h)-1)
    return Gout[1:m+1]

def J(U):
    Jout = np.zeros((m+2,m+2))
    for i in range(1,m+1):
        Jout[i,i-1] = eps/h2-2*U[i]/(2*h)
        Jout[i,i] = -2*eps/h2+(U[i+1]-U[i-1])/(2*h)-1
        Jout[i,i+1] = eps/h2+2*U[i]/(2*h)
    return Jout[1:m+1,1:m+1]

# problem parameters
alpha = -1
beta = 1.5
eps = 0.01

a = 0
b = 1

# discritization parameters
for e in [1,2,3,4,5,6,8]:
    m = 10*2**e-1
    x = np.linspace(a,b,m+2)
    h = (b-a)/(m+1)
    h2 = h**2

    # initial guess
    xb = (a+b-alpha-beta)/2
    w0 = (a-b+beta-alpha)/2
    U = x - xb + w0*np.tanh(w0*(x-xb)/(2*eps))

    # Newton's method
    for k in range(25):
        print(k)
        delta = np.linalg.solve(J(U),-G(U))
        U[1:m+1] += delta

        print(max(abs(delta)))
        if max(abs(delta)) < 10e-14:
            break

    plt.figure()
    plt.scatter(x,U)
    plt.savefig('img/4/'+str(m+1)+'.pdf')
#<endTeX>


# -*- encoding: utf-8 -*-
'''
@File    :   task4.py
@Modify Time      @Author       @Desciption
------------      -------       -----------
2021/6/6 15:47   Jonas           None
'''

import math

import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
from scipy.spatial.distance import cdist

training_data = pd.read_csv("nonParamTrain.txt", sep="   ")
test_data = pd.read_csv("nonParamTest.txt", sep="   ")

training_data.columns = test_data.columns = ["value"]

x_min = -4
x_max = 8

# 4a) Histogram
def plot_histo():
    histo_size = [0.02, 0.5, 2]

    for i, s in enumerate(histo_size):
        plt.figure(i)
        # number of bins = training_data.max().value / s
        training_data.plot.hist(by="value", bins=math.ceil(training_data.max().value / s))
        plt.xlabel("x")
        plt.title("Histogram with bin size {}".format(s))
        plt.xlim(x_min, x_max)

def gaussian_kernel(x, data, sigma):
    numerator = np.sum(np.exp(-(x - data) ** 2 / (2 * sigma ** 2)))
    denominator = np.sqrt(2 * math.pi) * sigma
    return numerator / denominator

# 4b) Kernel Density Estimate
def gaussian_KDE():
    sigmas = [0.03, 0.2, 0.8]
    steps = (x_max - x_min) / 500
    x = np.arange(x_min, x_max, steps)
    # x = np.sort(test_data.values, axis=0)
    plt.figure()
    for sigma in sigmas:

        # get log-likelihood
        # lecture05 slides48
        y = np.empty(training_data.values.shape[0])
        for i, val in enumerate(training_data.values):
            y[i] = gaussian_kernel(val, training_data.values, sigma)

        print("The train log−likelihood for sigma = {} is {}".format(str(sigma), str(np.sum(np.log(y)))))

        # get plots
        y = np.empty(x.shape)
        for i, val in enumerate(x):
            y[i] = gaussian_kernel(val, training_data.values, sigma)

        print("The test log−likelihood for sigma = {} is {}".format(str(sigma), str(np.sum(np.log(y)))))

        plt.plot(x, y, label="$\sigma=$" + str(sigma))
        plt.ylabel('Density')
        plt.xlabel('x')

    plt.legend()
    plt.show()

# 4c) K-Nearest Neighbour
def knn():
    ks = [2, 8, 35]
    steps = (x_max - x_min) / 300
    x = np.arange(x_min, x_max, steps)
    # calculate pairwise distances
    x_dist = cdist(x.reshape(x.shape[0], 1),
                   training_data.values.reshape(training_data.values.shape[0], 1),
                   metric="euclidean")

    for k in ks:
        y = np.empty(x.shape)
        for i, val in enumerate(x_dist):
            # find nearest k points and take point with greatest distance as Volume size
            # this assumes the distance matrix was computed with two different vectors
            # use k+1 for train data
            # np.argpartition(val, range(k))[:k] means top k element
            V = val[np.argpartition(val, range(k))[:k]][-1]
            # calculate density
            y[i] = k / (training_data.values.shape[0] * V * 2)

        print("The log−likelihood for k={} is {}"
              .format(k, np.sum(np.log(y))))

        plt.plot(x, y, label="k={}".format(k))
        plt.ylabel('Density')
        plt.xlabel('x')

    plt.legend()
    plt.show()

# plot_histo()
gaussian_KDE()
knn()
plt.show()


from __future__ import division

from copy import deepcopy

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

def tabular_soft_q_iter(R, T, maxiter=5000, verbose=False, Q_init=None, learning_rate=1, ftol=0, gamma=0.99):
  '''Adapted from https://github.com/rddy/isql/blob/master/1.0-tabular-ime.ipynb'''

  T = deepcopy(T) # s',s,a
  T = np.exp(T)
  T = np.swapaxes(T, 0, 1) # s,s',a
  T = np.swapaxes(T, 1, 2) # s,a,s'

  n, m = R.shape[:2]
  Q = np.zeros((n, m)) if Q_init is None else deepcopy(Q_init)
  prevQ = deepcopy(Q)
  if verbose:
    diffs = []
  for iter_idx in range(maxiter):
    V = scipy.special.logsumexp(prevQ, axis=1)
    V_broad = V.reshape((1, 1, n))
    Q = np.sum(T * (R + gamma * V_broad), axis=2)
    Q = (1 - learning_rate) * prevQ + learning_rate * Q
    diff = np.mean((Q - prevQ)**2)/(np.std(Q)**2)
    if verbose:
      diffs.append(diff)
    if diff < ftol:
      break
    prevQ = deepcopy(Q)
  if verbose:
    plt.xlabel('Number of Iterations')
    plt.ylabel('Avg. Squared Bellman Error')
    plt.title('Soft Q Iteration')
    plt.plot(diffs)
    plt.yscale('log')
    plt.show()

  Q -= scipy.special.logsumexp(Q, axis=1, keepdims=True)

  return Q


import numpy as np
from utils.activations import sigmoid

class logistic:
    def __init__(self, max_iter=100, tol=1e-3, learning_rate=0.01, verbose=False):
        self.max_iter = max_iter
        self.tol = tol
        self.learning_rate = learning_rate
        self.b = 0
        self.verbose = verbose
        self.log_step = max_iter/10

    def propagate(self, X, Y):
        m = X.shape[1]

        # forward propagation
        Z = np.dot(self.W.T, X) + self.b
        A = sigmoid(Z)
        J = -np.sum(Y * np.log(A) + (1-Y) * np.log(1-A))/m

        # back propagation
        dz = (A - Y)  # dL/dz
        dw = 1/m * np.dot(X, dz.T)
        db = 1/m * np.sum(dz)

        grads = {"dw": dw,
                 "db": db}

        return grads, J

    def fit(self, X, Y):
        n = X.shape[0]
        self.W = np.zeros((n, 1))

        for i in range(self.max_iter):
            grads, J = self.propagate(X, Y)

            dw = grads["dw"]
            db = grads["db"]

            # update weights & bias
            self.W -= self.learning_rate * dw
            self.b -= self.learning_rate * db

            if(self.verbose and i % self.log_step == 0):
                print(f'Iteration {i} cost J = {J}')

    def predict(self, X):
        pred = sigmoid(np.dot(self.W.T, X) + self.b)

        pred = (pred > 0.5).astype(float)

        return pred


import matplotlib.pylab as pl
import random
import math
import numpy as np

"""
NumRep Checkpoint 1: simulating 1000 muon decays
Simon McLaren
"""

# Open output file for writing
muonDecayTimesOutputFile = open("muondecaytimes.out", "w")

# Returns a value for the muon lifetime probability density at a specified time
def evaluateMuonDistribution(time, tau):
    return float((1/tau)*math.exp(-(time/tau)))

muonLifetime = 2.2 # in microseconds
numberofdecays = 1000
simulatedDecayTimes = []
# Parameters for sampling a random number from the distribution
fmax = evaluateMuonDistribution(0, muonLifetime)
mint = 0.
maxt = 22.

# Simulate 1000 muon lifetimes
for i in range(numberofdecays):
    # Draw a random time from the distribution using the box method
    y2 = 10.
    y1 = 1.
    while y2 > y1:
        randomt = mint + (maxt - mint) * random.random()
        y1 = evaluateMuonDistribution(randomt, muonLifetime)
        y2 = fmax * random.random()
    simulatedDecayTimes.append(randomt)
    muonDecayTimesOutputFile.write("{0:f}\n".format(randomt))

simulatedDecayTimes = np.array(simulatedDecayTimes)
estimatedMuonLifetime = np.mean(simulatedDecayTimes)

# Produce histogram
pl.hist(simulatedDecayTimes[:], bins = 100, range = [mint,maxt])
pl.savefig("muondecaytimeshistogram.png")

# Print results
print "The estimated muon lifetime is %f microseconds." %(estimatedMuonLifetime)
print "The standard deviation is %f." %(np.std(simulatedDecayTimes))

muonDecayTimesOutputFile.close()


# Modelling spread of disease
# https://www.maa.org/press/periodicals/loci/joma/the-sir-model-for-spread-of-disease-the-differential-equation-model
import random
import numpy as np
import matplotlib.pyplot as plt
from random import uniform

def w_choice(seq):
    """
    Takes an input on the form of [('A', 30), ('B', 40), ('C', 30)] and returns
    one of the items based on the probality from the second value in each tuple.
    Eg. B will get chosen 40% of the time while A and C will get chosen 30% of
    the time.
    """
    total_prob = sum(item[1] for item in seq)
    chosen = random.uniform(0, total_prob)
    cumulative = 0
    for item, probality in seq:
        cumulative += probality
        if cumulative > chosen:
            return item

class Disease:
    """
    Uses the SIR model to model the spread of a certain disease.

    At each time t, s(t) + i(t) + r(t) = 1
    """

    def __init__(self, N, b, k, susceptible, infected, recovered):
        """
        params:
            N = The total population
            b = The average _FIXED_ number of contacts per day per person.
            k = The average _FIXED_ fraction of infected individuals that will recover per day.
            susceptible = The initial amount of susceptible individuals
            infected = The initial amount of infected individuals
            recovered = The initial amount of recovered individuals
        """
        self.time = 0
        self._N = N
        self._b = b
        self._k = k
        self._susceptible = susceptible
        self._infected = infected
        self._recovered = recovered

    def N(self):
        return self._N

    def vaccinate(self, amount):
        """
        Vaccinates the specified amount of the population.
        """
        self.susceptible(self.susceptible() - amount)

    def susceptible(self, v=None):
        """
        The number of susceptible individuals.
        S = S(t)

        No one is added to this group due to births and immigrations begin ignored.
        An individual will leave this group once infected.
        """
        if v is None:
            return self._susceptible
        self._susceptible = v

    def susceptible_dt(self):
        """
        The rate of change for susceptible individuals.
        dS/dt = -b*s(t)*I(t)
        """
        return -1 * self._b * self.susceptible_fraction() * self.infected()

    def infected(self, v=None):
        """
        The number of infected individuals.
        I = I(t)

        Each infected individual infects b * s(t) new individuals.
        """
        if v is None:
            return self._infected
        self._infected = v

    def recovered(self, v=None):
        """
        The number of recovered individuals.
        R = R(t)
        """
        if v is None:
            return self._recovered
        self._recovered = v

    def susceptible_fraction(self):
        """
        The susceptible fraction of the population.

        s(t)
        """
        return self.fraction(self.susceptible())

    def susceptible_fraction_dt(self):
        """
        The rate of change for the susceptible fraction.
        ds/dt = -b*s(t)*i(t)
        """
        return -1 * self._b * self.susceptible_fraction() * self.infected_fraction()

    def recovered_fraction(self):
        """
        The recovered fraction of the population.

        r(t)
        """
        return self.fraction(self.recovered())

    def recovered_fraction_dt(self):
        """
        The rate of change for the recovered fraction of the population.
        dr/dt = k*i(t)
        """
        return self._k * self.infected_fraction()

    def infected_fraction(self):
        """
        The infected fraction of the population.

        i(t)
        """
        return self.fraction(self.infected())

    def infected_fraction_dt(self):
        """
        The rate of change for the infected fraction of the population.
        di/dt = b * s(t) * i(t) - k * i(t)
        di/dt = -ds/dt - dr/dt
        """
        return -1 * (self.susceptible_fraction_dt() + self.recovered_fraction_dt())

    def fraction(self, nr):
        """
        Returns the fraction of the total population.
        nr / N
        """
        return nr / self._N

    def number(self, fraction):
        """
        Returns the number of the total population
        fraction * N
        """
        return fraction * self._N

    def step(self):
        """
        Steps forward one unit in time.
        """
        i_dt = self.number(self.infected_fraction_dt())
        r_dt = self.number(self.recovered_fraction_dt())
        s_dt = self.number(self.susceptible_fraction_dt())
        self.susceptible(self.susceptible() + s_dt)
        self.recovered(self.recovered() + r_dt)
        self.infected(self.infected() + i_dt)

class City:
    """
    City.
    """

    def __init__(self, name, position, N, b, k, susceptible, infected, recovered):
        """
        params:
            name = The name of the city.
            population = The population of the city
            position = The position of the city
        """
        self._position = position
        self._name = name
        self._disease = Disease(N=N, b=b, k=k, susceptible=susceptible, infected=infected, recovered=recovered)
        self.Y_susceptible = []
        self.Y_infected = []
        self.Y_recovered = []

    def population(self):
        """
        Returns the population of the city.
        """
        return self.disease().N()

    def position(self):
        """
        Returns the position of the city.
        """
        return self.position

    def disease(self):
        return self._disease

    def vaccinate(self, n):
        """
        Vaccinates n people of the population.
        """
        self.disease().vaccinate(n)

    def name(self):
        return self._name

    def step(self, t, _print=False):
        """
        Steps forward one step in time.
        params:
            t = The current timestamp (Used when printing).
            _print = If output should be printed to the console
        """
        d = self.disease()
        self.log(d, t, _print)
        d.step()

    def log(self, d, t, _print):
        """
        Logs the current state of the disease for later plotting and/or dumping
        to the console.
        """
        self.Y_susceptible.append(d.susceptible_fraction())
        self.Y_infected.append(d.infected_fraction())
        self.Y_recovered.append(d.recovered_fraction())
        if _print:
            print("S("+str(t)+")", "%0.2f" % d.susceptible(), "\ts("+str(t)+")=", "%0.2f" % d.susceptible_fraction())
            print("I("+str(t)+")", "%0.2f" % d.infected(), "\ti("+str(t)+")=", "%0.2f" % d.infected_fraction())
            print("R("+str(t)+")", "%0.2f" % d.recovered(), "\tr("+str(t)+")=", "%0.2f" % d.recovered_fraction())
            print()

    def plot(self):
        """
        Plots the progress of the diesease for this City.
        """
        _ys, = plt.plot(self.Y_susceptible, label='s(t)')
        _yi, = plt.plot(self.Y_infected, label="i(t)")
        _yr, = plt.plot(self.Y_recovered, label="r(t)")
        plt.legend(handles=[_ys, _yi, _yr])
        plt.title(self.name())
        plt.show()

    def plot_get_infected(self):
        _yi, = plt.plot(self.Y_infected, label="i(t) - "+str(self.name()))
        return _yi

def example_graph():
    """
    Example graph that is the same as the one found at the link:
    https://www.maa.org/press/periodicals/loci/joma/the-sir-model-for-spread-of-disease-the-differential-equation-model
    """
    c = City(name="Sweden", position=(0,0), N=7900000, b=1/2, k=1/3, susceptible=7900000, infected=10, recovered=0)

    for t in range(140):
        c.step(t)

    c.plot()

def example_cities():
    b = 0.99
    k = 1/10
    cities = [
        City(name="Stockholm", position=(0,0),      N=1400000, b=b, k=k, susceptible=1400000, infected=1, recovered=0),
        City(name="Göteborg",  position=(-50, -50), N=1000000, b=b, k=k, susceptible=1000000, infected=0,  recovered=0),
        City(name="Umeå",      position=(-4, 100),  N=100000,  b=b, k=k, susceptible=100000,  infected=0,  recovered=0),
        City(name="Örebro",    position=(-10, 5),   N=350000,  b=b, k=k, susceptible=350000,  infected=0,  recovered=0)
    ]

    print(cities)
    for c in cities:
        print(cities[:].remove(c))

    for t in range(140):
        for c in cities:
            c.step(t)

    plt.legend(handles=[c.plot_get_infected() for c in cities])
    plt.show()

example_cities()


import numpy as np

class TensorDP:

    def __init__(self,
                 gamma=1.0,
                 error_tol=1e-5):
        self.gamma = gamma
        self.error_tol = error_tol

        # Following attributes will be set after call "set_env()"

        self.env = None  # environment
        self.policy = None  # policy
        self.ns = None  # Num. states
        self.na = None  # Num. actions
        self.P = None  # Transition tensor
        self.R = None  # Reward tensor

    def set_env(self, env, policy=None):
        self.env = env
        if policy is None:
            self.policy = np.ones([env.nS, env.nA]) / env.nA

        self.ns = env.nS
        self.na = env.nA
        self.P = env.P_tensor  # Rank 3 tensor [num. actions x num. states x num. states]
        self.R = env.R_tensor  # Rank 2 tensor [num. actions x num. states]

        print("Tensor DP agent initialized")
        print("Environment spec:  Num. state = {} | Num. actions = {} ".format(env.nS, env.nA))

    def reset_policy(self):
        self.policy = np.ones([self.ns, self.na]) / self.na

    def set_policy(self, policy):
        assert self.policy.shape == policy.shape
        self.policy = policy

    def get_r_pi(self, policy):
        r_pi = (policy * self.R).sum(axis=-1)  # [num. states x 1]
        return r_pi

    def get_p_pi(self, policy):
        p_pi = np.einsum("na,anm->nm", policy, self.P)  # [num. states x num. states]
        return p_pi

    def policy_evaluation(self, policy=None, v_init=None):
        """
        :param policy: policy to evaluate (optional)
        :param v_init: initial value 'guesstimation' (optional)
        :param steps: steps of bellman expectation backup (optional)
        if none, repeat the backup until converge.

        :return: v_pi: value function of the input policy
        """
        if policy is None:
            policy = self.policy

        r_pi = self.get_r_pi(policy)  # [num. states x 1]
        p_pi = self.get_p_pi(policy)  # [num. states x num. states]

        if v_init is None:
            v_old = np.zeros(self.ns)
        else:
            v_old = v_init

        while True:
            # perform bellman expectation back
            v_new = r_pi + self.gamma * np.matmul(p_pi, v_old)

            # check convergence
            bellman_error = np.linalg.norm(v_new - v_old)
            if bellman_error <= self.error_tol:
                break
            else:
                v_old = v_new

        return v_new

    def policy_improvement(self, policy=None, v_pi=None):
        if policy is None:
            policy = self.policy

        if v_pi is None:
            v_pi = self.policy_evaluation(policy)

        # Compute Q_pi(s,a) from V_pi(s)
        r_pi = self.get_r_pi(policy)
        q_pi = r_pi + self.P.dot(v_pi)  # q_pi = [num.action x num states]

        # Greedy improvement
        policy_improved = np.zeros_like(policy)
        policy_improved[np.arange(q_pi.shape[1]), q_pi.argmax(axis=0)] = 1
        return policy_improved

    def policy_iteration(self, policy=None):
        if policy is None:
            pi_old = self.policy
        else:
            pi_old = policy

        info = dict()
        info['v'] = list()
        info['pi'] = list()
        info['converge'] = None

        steps = 0
        converged = False
        while True:
            v_old = self.policy_evaluation(pi_old)
            pi_improved = self.policy_improvement(pi_old, v_old)
            steps += 1

            info['v'].append(v_old)
            info['pi'].append(pi_old)

            # check convergence
            policy_gap = np.linalg.norm(pi_improved - pi_old)

            if policy_gap <= self.error_tol:
                if not converged:  # record the first moment of within error tolerance.
                    info['converge'] = steps
                break
            else:
                pi_old = pi_improved
        return info

    def value_iteration(self, v_init=None, compute_pi=False):
        """
        :param v_init: (np.array) initial value 'guesstimation' (optional)
        :param compute_pi: (bool) compute policy during VI
        :return: v_opt: the optimal value function
        """

        if v_init is not None:
            v_old = v_init
        else:
            v_old = np.zeros(self.ns)

        info = dict()
        info['v'] = list()
        info['pi'] = list()
        info['converge'] = None

        steps = 0
        converged = False

        while True:
            # Bellman optimality backup
            v_improved = (self.R.T + self.gamma * self.P.dot(v_old)).max(axis=0)
            info['v'].append(v_improved)

            if compute_pi:
                # compute policy from v
                # 1) Compute v -> q
                q_pi = (self.R.T + self.gamma * self.P.dot(v_improved))

                # 2) Construct greedy policy
                pi = np.zeros_like(self.policy)
                pi[np.arange(q_pi.shape[1]), q_pi.argmax(axis=0)] = 1
                info['pi'].append(pi)

            steps += 1

            # check convergence
            policy_gap = np.linalg.norm(v_improved - v_old)

            if policy_gap <= self.error_tol:
                if not converged:  # record the first moment of within error tolerance.
                    info['converge'] = steps
                break
            else:
                v_old = v_improved
        return info


"""

http://scipp.ucsc.edu/~haber/ph116A/Rotation2.pdf
https://math.stackexchange.com/questions/3501114/understanding-a-consequence-of-eulers-rotation-theorem
https://en.wikipedia.org/wiki/Euler%27s_rotation_theorem
https://en.wikipedia.org/wiki/Rotation_formalisms_in_three_dimensions
https://en.wikipedia.org/wiki/Euler_angles
https://en.wikipedia.org/wiki/Euclidean_vector
https://en.wikipedia.org/wiki/Direction_cosine
https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2012/07/euler-angles1.pdf
https://www.mathworks.com/help/aerotbx/ug/angle2dcm.html
https://en.wikipedia.org/wiki/Rodrigues'_rotation_formula
https://en.wikipedia.org/wiki/Axis%E2%80%93angle_representation
https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation

"""

import math
import numpy as np
import sympy
import sympy.abc
from scipy.stats import special_ortho_group

class Quat:
    def __init__(self, x, y, z, w):
        self.x = x
        self.y = y
        self.z = z
        self.w = w

    def __str__(self):
        return sympy.pretty([self.x, self.y, self.z, self.w])

    @staticmethod
    def from_axis_angle(axis, angle):
        s = sympy.sin(angle/2)
        c = sympy.cos(angle/2)
        q = Quat(axis[0]*s, axis[1]*s, axis[2]*s, c)
        return q

    def rotation_matrix(self):
        x = self.x
        y = self.y
        z = self.z
        w = self.w
        x2 = x*x
        y2 = y*y
        z2 = z*z
        xy = x*y
        xz = x*z
        yz = y*z
        wx = w*x
        wy = w*y
        wz = w*z
        s = 1
        return sympy.Matrix([[1 - 2*s*(y2+z2), 2*s*(xy-wz), 2*s*(xz+wy)],
                             [2*s*(xy+wz), 1 - 2*s*(x2+z2), 2*s*(yz-wx)],
                             [2*s*(xz-wy), 2*s*(yz+wx), 1 - 2*s*(x2+y2)]])

"""

The Euler rotation theorem states that for any rotation around an axis in 3D
(represented by a rotation matrix) there will be an axis that is unchanged by the rotation

Mathematically, this means the following:
R - rotation matrix
v - a vector that represents an axis of rotation
There is such a vector v where
R*v = R*R*v = R*R*R*v ...

To find that vector, we need to find the eigenvalues/eigenvectors of R
Since R is a NxN matrix where N is the dimension, we are going to get
N eigenvalues with their magnitude to be 1.

One of these eigenvalue will be (1+0i), and that corresponding eigenvector
will be the fixed axis of rotation because its eigenvalue is 1

ew = 1
R^n * ev = ew^n * ev  = ev

Generalizing this, for even dimensional space (2, 4, 6 ...),
there is no fixed axis of rotation because of the fundamental
theorem of algebra says that the complex roots always comes in pair,
so there is no eigenvalue of (1+0i) for those

The corollary is that in 3D, any number of rotations can be represented
by one rotation around a fixed axis

"""

def test_rotation_theorem(n):
    R = special_ortho_group.rvs(n)

    print("Rotation Matrix {}x{}".format(n, n))
    print(R)

    print("Eigenvalues and Eigenvectors")
    ew, ev = np.linalg.eig(R)
    vf = None
    for i in range(len(ew)):
        if abs(np.real(ew[i])-1) < 1e-6:
            vf = ev[:, i]
        print(ew[i])
        print(ev[:, i])
        print()

    print("Fixed axis of rotation")
    print(vf)
    if vf is not None:
        print(np.dot(R, vf[:]))
    print()

def make_matrix_from_angles(angle, axis, sym):
    if not sym:
        c = math.cos(angle)
        s = math.sin(angle)
    else:
        c = sympy.cos(angle)
        s = sympy.sin(angle)
    if axis == 'X':
        M = [[1, 0, 0], [0, c, -s], [0, s, c]]
    elif axis == 'Y':
        M = [[c, 0, s], [0, 1, 0], [-s, 0, c]]
    else:
        M = [[c, -s, 0], [s, c, 0], [0, 0, 1]]

    if sym:
        M = sympy.simplify(sympy.Matrix(M))

    return M

def extract_matrix_angle(M, conv):
    if conv == 'XYZ':
        t1 = math.atan2(M[1][2], M[2][2])
        c2 = math.sqrt(M[0][0]**2 + M[0][1]**2)
        t2 = math.atan2(-M[0][2], c2)
        s1 = math.sin(t1)
        c1 = math.cos(t1)
        t3 = math.atan2(s1*M[2][0] - c1*M[1][0], c1*M[1][1] - s1*M[2][1])
        t1, t2, t3 = -t1, -t2, -t3
    elif conv == 'ZYX':
        t1 = math.atan2(M[2][1], M[2][2])
        t2 = math.asin(-M[2][0])
        t3 = math.atan2(M[1][0], M[0][0])
    else:
        assert 0
    return (t1, t2, t3)

def angle2dcm(yaw, pitch, roll, conv='ZYX'):
    M = None
    if conv == 'RPY' or conv == 'ZYX':
        Z = make_matrix_from_angles(yaw, 'Z', False)
        Y = make_matrix_from_angles(pitch, 'Y', False)
        X = make_matrix_from_angles(roll, 'X', False)
        M = np.dot(Z, np.dot(Y, X))
    else:
        assert 0

    return np.linalg.inv(M)

def vector2dcm(E, N):
    n = E.shape[0]
    if n == 2:
        e1 = E[:, 0]
        e2 = E[:, 1]
        n1 = N[:, 0]
        n2 = N[:, 1]
        return np.array([
            [np.dot(n1, e1), np.dot(n1, e2)],
            [np.dot(n2, e1), np.dot(n2, e2)],
        ])
    elif n == 3:
        e1 = E[:, 0]
        e2 = E[:, 1]
        e3 = E[:, 2]
        n1 = N[:, 0]
        n2 = N[:, 1]
        n3 = N[:, 2]
        return np.array([
            [np.dot(n1, e1), np.dot(n1, e2), np.dot(n1, e3)],
            [np.dot(n2, e1), np.dot(n2, e2), np.dot(n2, e3)],
            [np.dot(n3, e1), np.dot(n3, e2), np.dot(n3, e3)],
        ])
    assert 0

def rodrigues_rotation_matrix(axis, angle):
    a1 = axis[0]
    a2 = axis[1]
    a3 = axis[2]
    K = sympy.Matrix([[0, -a3, a2], [a3, 0, -a1], [-a2, a1, 0]])
    I = sympy.eye(3)
    return sympy.trigsimp(sympy.expand(I + K*sympy.sin(angle) + np.dot(K, K)*(1-sympy.cos(angle))))

"""

A frame of reference can be constructed from 3 angles conventionally denoted as

(alpha, beta, gamma) or
(psi, theta, phi)

There are 12 different sequence of rotation axes one can make with 3 angles
and are classified into 2 categories
Proper Euler angles (z-x-z, x-y-x, y-z-y, z-y-z, x-z-x, y-x-y)
Tait–Bryan angles   (x-y-z, y-z-x, z-x-y, x-z-y, z-y-x, y-x-z)

These represent rotation of an axis around the angle alpha, beta, or gamma
and we multiply all of those matrices generated together to get the concatenated matrix

These are active rotations, to find the passive rotations, transpose the matrix (which is the inverse)

"""

def test_matrix_angle_conventions():
    print("Matrix Angle Conventions")

    X1 = make_matrix_from_angles(sympy.abc.a, 'X', True)
    Y1 = make_matrix_from_angles(sympy.abc.a, 'Y', True)
    Z1 = make_matrix_from_angles(sympy.abc.a, 'Z', True)
    X2 = make_matrix_from_angles(sympy.abc.b, 'X', True)
    Y2 = make_matrix_from_angles(sympy.abc.b, 'Y', True)
    Z2 = make_matrix_from_angles(sympy.abc.b, 'Z', True)
    X3 = make_matrix_from_angles(sympy.abc.c, 'X', True)
    Y3 = make_matrix_from_angles(sympy.abc.c, 'Y', True)
    Z3 = make_matrix_from_angles(sympy.abc.c, 'Z', True)

    print()
    print("Proper Euler Angles")
    print()
    print("ZXZ")
    sympy.pprint(np.dot(X1, np.dot(Z2, X3)))

    print("XYX")
    sympy.pprint(np.dot(X1, np.dot(Y2, X3)))

    print("YXY")
    sympy.pprint(np.dot(Y1, np.dot(X2, Y3)))

    print("YZY")
    sympy.pprint(np.dot(Y1, np.dot(Z2, Y3)))

    print("ZYZ")
    sympy.pprint(np.dot(Z1, np.dot(Y2, Z3)))

    print("ZXZ")
    sympy.pprint(np.dot(Z1, np.dot(X2, Z3)))

    print()
    print("Tait-Bryan Angles")
    print()
    print("XZY")
    sympy.pprint(np.dot(X1, np.dot(Z2, Y3)))

    print("XYZ")
    sympy.pprint(np.dot(X1, np.dot(Y2, Z3)))

    print("YXZ")
    sympy.pprint(np.dot(Y1, np.dot(X2, Z3)))

    print("YZX")
    sympy.pprint(np.dot(Y1, np.dot(Z2, X3)))

    print("ZYX")
    sympy.pprint(np.dot(Z1, np.dot(Y2, X3)))

    print("ZXY")
    sympy.pprint(np.dot(Z1, np.dot(X2, Y3)))

    print()

"""

Given a rotation matrix, we want to extract
the 3 angles that make it up, the angles
returned depends on what convention we used
generally we return the angles in the [-pi, pi] range

"""

def test_matrix_angle_extraction():
    print("Matrix Angle Extraction")
    R1 = make_matrix_from_angles(0.3, 'X', False)
    R2 = make_matrix_from_angles(-0.5, 'Y', False)
    R3 = make_matrix_from_angles(0.6, 'Z', False)
    print(extract_matrix_angle(np.dot(R1, np.dot(R2, R3)), 'XYZ'))

    R1 = make_matrix_from_angles(-0.2, 'X', False)
    R2 = make_matrix_from_angles(-1.4, 'Y', False)
    R3 = make_matrix_from_angles(-math.pi, 'Z', False)
    print(extract_matrix_angle(np.dot(R1, np.dot(R2, R3)), 'XYZ'))

    R1 = make_matrix_from_angles(2.1, 'X', False)
    R2 = make_matrix_from_angles(-1.2, 'Y', False)
    R3 = make_matrix_from_angles(0.5, 'Z', False)
    print(extract_matrix_angle(np.dot(R1, np.dot(R2, R3)), 'XYZ'))

    R1 = make_matrix_from_angles(1.1, 'Z', False)
    R2 = make_matrix_from_angles(0.5, 'Y', False)
    R3 = make_matrix_from_angles(0.3, 'X', False)
    print(extract_matrix_angle(np.dot(R1, np.dot(R2, R3)), 'ZYX'))

    R1 = make_matrix_from_angles(-0.8, 'Z', False)
    R2 = make_matrix_from_angles(-1.2, 'Y', False)
    R3 = make_matrix_from_angles(1.3, 'X', False)
    print(extract_matrix_angle(np.dot(R1, np.dot(R2, R3)), 'ZYX'))

    print()

"""

A direction cosine matrix is making a rotation matrix
out of a given set of angles commonly referred to as (yaw, pitch, roll)

The direction cosine matrix name comes from constructing the cosine out of a set of vectors,
which when multiplied together can give us a rotation matrix

Given a vector P and Q, we want to find the angle between them:
dot(P, Q) = cos(angle) assuming P/Q are normalized which they usually are

Some common conventions:
RPY is used in airplanes which is the ZYX convention

Passive rotation is used to specify the rotation matrix
so need the inverse of the standard construction of the rotation matrix

"""

def test_angle2dcm():
    print("Direction Cosine Matrix from Angles")
    yaw = 0.7854
    pitch = 0.1
    roll = 0
    dcm = angle2dcm(yaw, pitch, roll)
    print(dcm)

    print(angle2dcm(0, 0, 0))

"""

If we are not given a set of angles to construct a direction cosine matrix
but two sets of orthogonal unit basis vectors
(e1, e2)     (n1, n2)     2D
(e1, e2, e3) (n1, n2, n3) 3D

We want to construct a matrix that transforms vectors from basis E to N.
We can construct a direction cosine matrix A as follows:
A_ij = dot(n_i, e_j)

This matrix that we constructed from this procedure is equivalent to the following transformation:

E = orientation matrix of basis vectors e_i
N = orientation matrix of basis vectors n_i
A = N^-1*E

"""

def test_vector2dcm(n):
    E = special_ortho_group.rvs(n)
    N = special_ortho_group.rvs(n)
    I = np.eye(n)

    EN = vector2dcm(E, N)
    NE = vector2dcm(N, E)

    print("Direction Cosine Matrix from Vectors")

    print("The basis matrix")
    print(E)
    print()
    print(N)
    print()

    # Transforming to the same basis gives us the identity matrix as the transformation matrix
    print("Transforming to the same Basis")
    print(vector2dcm(E, E))
    print(vector2dcm(N, N))
    print()

    # The basis transformation E -> N is the inverse of N <- E and vice versa
    # Since this is a orthonormal matrix, they are transpose of each others
    print("From <-> To")
    print(EN)
    print(np.dot(np.linalg.inv(N), E))
    print()
    print(NE)
    print(np.dot(np.linalg.inv(E), N))
    print()

    # Transforming from the standard basis I -> N gives us the transformation N^-1
    print("Identity -> To")
    print(np.linalg.inv(E))
    print(vector2dcm(I, E))
    print()
    print(np.linalg.inv(N))
    print(vector2dcm(I, N))
    print()

"""

If we want to rotate around an arbitrary axis instead of a product of rotations around the standard axes,
we can use the rodrigues rotation formula.

The matrix form of the formula can be derived using the exponential mapping

R = exp(K*theta)
K = w x v where w is the unit vector and v is the vector to be rotated
K is written out as a cross product matrix

We can expand the matrix exponential as a taylor series
I + theta*K + (theta*K)^2/2! + (theta*K)^3/3! ...
Because K is skew-symmetric and the sum of squares of the above diagonal is 1, the characteristic polynomial
implies that K^3 = -K, K^4 = -K^2 ...
So we can group these together and simplify to get

R = I + sin(theta)*K + (1-cos(theta))*K^2

"""

def test_rodrigues_formula():
    print()
    print("Rodrigues Rotation Formula")
    # gives the standard euler rotation matrix around X, Y, Z axes
    print("\nX Axis\n")
    sympy.pprint(rodrigues_rotation_matrix([1, 0, 0], sympy.abc.theta))
    print("\nY Axis\n")
    sympy.pprint(rodrigues_rotation_matrix([0, 1, 0], sympy.abc.theta))
    print("\nZ Axis\n")
    sympy.pprint(rodrigues_rotation_matrix([0, 0, 1], sympy.abc.theta))

    # general formula for rotation around arbitrary axis
    print("\nArbitrary Axis\n")
    sympy.pprint(rodrigues_rotation_matrix(
        [sympy.abc.x, sympy.abc.y, sympy.abc.z], sympy.abc.theta))

"""

Quaternions can be used as an alternative to Euler angles, using standard quaternion construction formulas
we can show that it gives us the same formula as the rotation matrices generated from Euler angles

"""

def test_quaternion():
    # gives standard euler rotation matrix around X, Y, Z axes
    X = Quat.from_axis_angle([1, 0, 0], sympy.abc.theta)
    Y = Quat.from_axis_angle([0, 1, 0], sympy.abc.theta)
    Z = Quat.from_axis_angle([0, 0, 1], sympy.abc.theta)
    R = Quat.from_axis_angle(
        [sympy.abc.x, sympy.abc.y, sympy.abc.z], sympy.abc.theta)
    print("\nQuaternion\n")
    print("\nX Axis\n")
    sympy.pprint(sympy.simplify(X.rotation_matrix()))
    print("\nY Axis\n")
    sympy.pprint(sympy.simplify(Y.rotation_matrix()))
    print("\nZ Axis\n")
    sympy.pprint(sympy.simplify(Z.rotation_matrix()))

    # this gives the same formula as the rodrigues formula
    print("\nArbitrary Axis\n")
    s = R.rotation_matrix()
    s = s.subs(sympy.sin(sympy.abc.theta/2)**2,
               (1-sympy.cos(sympy.abc.theta))/2)
    s = sympy.expand(sympy.simplify(s))
    sympy.pprint(s)

sympy.init_printing()

for i in range(2, 6):
    test_rotation_theorem(i)
test_matrix_angle_conventions()
test_matrix_angle_extraction()
test_angle2dcm()
test_vector2dcm(2)
test_vector2dcm(3)
test_rodrigues_formula()
test_quaternion()