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

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.integrate import quad
import seaborn as sns
from numba import jit
import time
plt.style.use('seaborn')

@jit
def Fokker_Planck_Equation(x, N, u, v, s):
    """
    Returns a function for stationary distribution of Moran process.
    """
    return np.power(x, N*v - 1)*np.power((1 - x), (N*u - 1))*np.exp(N*s*x)

@jit
def Fokker_Planck_Mean(x, N, u, v, s):
    """
    Returns the mean of Moran process at stationary using Fokker-Planck.
    """
    return x*np.power(x, N*v - 1)*np.power((1 - x), (N*u - 1))*np.exp(N*s*x)

@jit
def Fixation_Prob_Moran(N, w_a, w_A):
    """
    Returns an approximation for fixation probability for Moran process.
    """
    return ((w_A/w_a) - 1)/(np.power(w_A/w_a, N) - 1)

@jit
def Fixation_Prob_Fisher(N, s):
    """
    Returns an approximation for fixation probability for Wright-Fisher process.
    """
    return (1 - np.exp(-s))/(1 - np.exp(-N*s))

@jit
def Moran_Process(N, w_a, w_A, u, v,
                  num_a, num_A, max_steps,
                  dt, num_sim):
    """
    N -- population size,
    w_a, w_A -- weights,
    u, v -- mutation rates,
    dt -- time step.
    """
    step = 0
    a_counts = []
    while step < max_steps:
        prob_birth = ((w_a * num_a)*(1 - u) / (w_a * num_a + w_A * num_A)
                      + (num_A * v) / (w_a * num_a + w_A * num_A)) * num_A / N
        prob_death = ((num_A)*(1 - v) / (w_a * num_a + w_A * num_A)
                      + (w_a * num_a)*u / (w_a * num_a + w_A * num_A)) * num_a / N
        r = np.random.rand(1)
        if r <= prob_birth*dt:
            num_a += 1
            num_A -= 1
        elif r <= (prob_birth + prob_death)*dt:
            num_a -= 1
            num_A += 1
        step += 1
        a_counts.append(num_a)
    return a_counts[-1]

# Plot the Fokker Planck distribution
population_sizes = [10, 100, 1000]
mutation_rates = [0.2, 0.02, 0.002]
s_list = [0.2, 0.02, 0.002]
w_A = 1
max_steps = 500000
dt = 1
num_sims = 1000
frequency = []
Fokker_Planck = []

for i in range(len(population_sizes)):
    x = np.linspace(0, 1, population_sizes[i])
    P = Fokker_Planck_Equation(x, population_sizes[i], mutation_rates[i],
                               mutation_rates[i], s_list[i])
    print("Population Size: ", population_sizes[i])
    simulations = [Moran_Process(population_sizes[i], 1+s_list[i], w_A,
                                 mutation_rates[i], mutation_rates[i],
                                 population_sizes[i]//2,
                                 population_sizes[i]-(population_sizes[i]//2),
                                 max_steps, dt, j) for j in range(num_sims)]
    Fokker_Planck.append(P)
    a_counts = np.asarray(simulations)
    frequency.append(a_counts)

fig, ax = plt.subplots(1,len(population_sizes), figsize=(20,5))
fig.suptitle('Stationary Distribution For Different Population Sizes',
             fontsize=18, y=1.12)
fig.text(0.5, -0.02, 'Frequency of a', ha='center', fontsize=16)
fig.text(-0.02, 0.5, 'Density', va='center', rotation='vertical', fontsize=16)
for i in range(len(population_sizes)):
    x = np.linspace(0, 1, population_sizes[i])
    data = frequency[i]/population_sizes[i]
    FP = Fokker_Planck[i]
    ax[i].plot(x, 2*FP, label='1', color='r')
    sns.kdeplot(data, ax=ax[i], label='2', color='b')
    ax[i].axvline(np.mean(data), color='teal', linestyle='dashed',
                  linewidth=2, alpha=0.75)
    ax[i].legend(['Fokker Planck solution', 'Simulations'], fontsize=14)
    ax[i].set_title('N = {0}'.format(population_sizes[i]), fontsize=14)
    ax[i].set_xlim([-0.01,1.01])
    plt.tight_layout()

# Plot the fixation probabilities
population_sizes = [10, 100, 1000, 10000]
mutation_rate = 0
s_list = [0.2, 0.02, 0.002, 0.0002]
w_A = 1
max_steps = 500000
dt = 1
num_sims = 1000
fix_prob_sim = []
fix_prob_Moran = []
fix_prob_wf = []

for i in range(len(population_sizes)):
    print("Population Size: ", population_sizes[i])
    f_M = Fixation_Prob_Moran(population_sizes[i], 1+s_list[i], w_A)
    f_WF = Fixation_Prob_Fisher(population_sizes[i], s_list[i])
    simulations = [Moran_Process(population_sizes[i], 1+s_list[i], w_A,
                                 mutation_rate, mutation_rate,
                                 1, population_sizes[i] - 1, max_steps,
                                 dt, j) for j in range(num_sims)]
    fix_prob = simulations.count(population_sizes[i])/num_sims
    fix_prob_sim.append(fix_prob)
    fix_prob_Moran.append(f_M)
    fix_prob_wf.append(f_WF)

plt.scatter(population_sizes, fix_prob_sim, color='b', label='simulation')
plt.scatter(population_sizes, fix_prob_Moran, color='r', label='Moran-Analytical')
plt.scatter(population_sizes, fix_prob_wf, color='orange', label='WF-Analytical')
plt.xscale('Log')
plt.xlabel('population size')
plt.ylabel('fixation probability at N')
plt.ylim([-0.01,0.3])
plt.legend()
plt.show()


class AverageFilter:

    def __init__(self):
        self.__k = 1
        self.__val = 0

    def update(self, value):
        alpha = (self.__k - 1) / self.__k
        self.__val = alpha * self.__val + (1 - alpha) * value
        self.__k = self.__k + 1
        return self.__val

    def get(self):
        return self.__val

class MovingAverageFilter:

    def __init__(self, window = 1):
        self.__val = 0
        self.__window = window
        self.__data = None

    def update(self, value):

        if self.__data is None:
            self.__data = [value] * self.__window
            self.__val = value

        self.__data.pop(0)
        self.__data.append(value)
        self.__val = self.__val + (value - self.__data[0]) / self.__window

        return self.__val

    def get(self):
        return self.__val

class LowPass1Filter:

    def __init__(self, alpha):
        self.__val = None
        self.__alpha = alpha

    def update(self, value):

        if self.__val == None:
            self.__val = value

        self.__val = self.__alpha  * self.__val + (1 - self.__alpha) * value

        return self.__val

    def get(self):
        return self.__val

if __name__ == "__main__":

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

    ### TEST AVERAGE

    def get_volt():
        return 14.0 + 4.0 * random.gauss(1,1)

    t = range(0,100,1)
    n = len(t)
    mu = AverageFilter()

    x_l = list()
    mu_l = list()

    for k in range(n):

        x = get_volt()
        mu.update(x)

        x_l.append(x)
        mu_l.append(mu.get())

    plt.figure(0)
    plt.plot(t,x_l)
    plt.plot(t,mu_l)
    plt.show()

    ### TEST MOVING AVERAGE

    def get_volt2(k):
        return 14.0 + 4.0 * random.gauss(1,1) + k * random.gauss(1,1)

    mu2 = MovingAverageFilter(10)

    x2_l = list()
    mu2_l = list()

    for k in range(n):

        x = get_volt2(k)
        mu2.update(x)

        x2_l.append(x)
        mu2_l.append(mu2.get())

    print(x2_l)
    print(mu2_l)

    plt.figure(1)
    plt.plot(t,x2_l)
    plt.plot(t,mu2_l)
    plt.show()

    ### TEST FIRST ORDER LOW PASS FILTER

    mu3 = LowPass1Filter(0.7)

    x3_l = list()
    mu3_l = list()

    for k in range(n):

        x = get_volt2(k)
        mu3.update(x)

        x3_l.append(x)
        mu3_l.append(mu3.get())

    print(x3_l)
    print(mu3_l)

    plt.figure(2)
    plt.plot(t,x3_l)
    plt.plot(t,mu3_l)
    plt.show()


# -*- coding: utf-8 -*-
"""
Lagrange Interpolation Routines.

@author: Juan Gomez
"""
from __future__ import division
from sympy import *
#
def LagrangPoly(x,order,i,xi=None):
    r"""Compute interpolant Lagrange Polynomials

    .. math::
      l_j(x)=\prod_{\substack{0<m\leq k\\ m\neq j}} \frac{x - x_m}{x_j - x_m}

    Parameters
    ----------
    x : Sympy symbol
        Variable for the interpolation.
    order : int
        Order of the polynomials.
    i : int
        Number of the polynomial according to `xi`.
    xi : (order + 1) list of float
        Position for the nodes.

    Returns
    -------
    poly : Sympy expression
        Interpolant polynomial for the `i`-th node in `xi`.

    Examples
    --------
    >>> from lagrange import LagrangPoly as la_poly
    >>> x = symbols('x')
    >>> pol0 = simplify(la_poly(x, 2, 0, [-1,1,0]))
    >>> pol1 = simplify(la_poly(x, 2, 1, [-1,1,0]))
    >>> pol2 = simplify(la_poly(x, 2, 2, [-1,1,0]))
    >>> print(pol0)
    x*(x - 1)/2

    >>> print(pol1)
    x*(x + 1)/2

    """
    if xi==None:
        xi = symbols('x:%d'%(order + 1))  # No entiendo esta condicion
    index = range(order + 1)
    index.pop(i)
    poly = prod([(x - xi[j])/(xi[i] - xi[j]) for j in index])
    return poly

# Run examples as tests
if __name__=="__main__":
    import doctest
    doctest.testmod()


# File: Function_Optimization.py
# Description: Optimization of smooth and non-smooth functions with 'BFGS' and 'differential evolution' methods
# Environment: PyCharm and Anaconda environment
#
# MIT License
# Copyright (c) 2018 Valentyn N Sichkar
# github.com/sichkar-valentyn
#
# Reference to:
# Valentyn N Sichkar. Machine Learning in Python // GitHub platform. DOI: 10.5281/zenodo.1345027

# Implementing the task
# Optimization of smooth and non-smooth functions

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

# Task 1 - smooth function
# Initial smooth function
# f(x) = sin(x / 5) * exp(x / 10) + 5 * exp(-x / 2)
# Function returns array with 'y' results from input array of 'x' for plotting by using 'np' instead of 'math'
def f_array(k):
    return np.sin(k / 5) * np.exp(k / 10) + 5 * np.exp(-k / 2)

# Finding minimum of smooth function with 'BFGS' method
# Setting initial point in form of 'ndarray' as 'minimize' function requires it in form of 'ndarray'
x0_start = np.array([2])
# print(type(x))  -->  <class 'numpy.ndarray'>
# print(x.shape)  -->  (1,)
# Finding minimum of the function from starting point 'x_start = 2'
# Using 'minimize' function from 'scipy.optimize' library
y0_min = optimize.minimize(f_array, x0_start, method='BFGS')
print(y0_min)  # fun = 1.7452682903449388, iterations = 6
print()

# Setting initial point in form of 'ndarray' as 'minimize' function requires it in form of 'ndarray'
x1_start = np.array([30])
# print(type(x))  -->  <class 'numpy.ndarray'>
# print(x.shape)  -->  (1,)
# Finding minimum of smooth function from starting point 'x_start = 30'
# Using 'minimize' function from 'scipy.optimize' library
y1_min = optimize.minimize(f_array, x1_start, method='BFGS')
print(y1_min)  # fun = -11.898894665981285, iterations = 6
print()

# Finding minimum of smooth function with 'differential evolution' method
# Searching in the range [1, 30]
# Setting the range for searching in form of tuple inside list as function requires it
x2_range = [(1, 30)]  # tuple inside list
# Finding minimum of smooth function
y2_min = optimize.differential_evolution(f_array, [(1, 30)])
print(y2_min)  # fun = -11.89889467, iterations = 5
print()

# Task 2 - non-smooth function
# Initial non-smooth function
# We take here the same function but only with integer results
# By using 'np.int_' we return 'numpy.ndarray' of integer numbers
def h_array(k):
    return np.int_(f_array(k))

# Finding minimum of non-smooth function with 'BFGS' method
# Setting initial point in form of 'ndarray' as 'minimize' function requires it in form of 'ndarray'
x3_start = np.array([30])
# print(type(x))  -->  <class 'numpy.ndarray'>
# print(x.shape)  -->  (1,)
# Finding minimum of the function from starting point 'x_start = 30'
# Using 'minimize' function from 'scipy.optimize' library
y3_min = optimize.minimize(h_array, x3_start, method='BFGS')
print(y3_min)  # fun = -5, iterations = 0
print()

# Finding minimum of non-smooth function with 'differential evolution' method
# Searching in the range [1, 30]
# Setting the range for searching in form of tuple inside list as function requires it
x4_range = [(1, 30)]  # tuple inside list
# Finding minimum of smooth function
y4_min = optimize.differential_evolution(h_array, [(1, 30)])
print(y4_min)  # fun = -11.0, iterations = 3
print()

# Plotting smooth and non-smooth functions with found points
# Creating a figure with subplots
figure, ax = plt.subplots(nrows=1, ncols=2)
# ax is (1, 2) np array and to make it easier to read we use 'flatten' function
# Or we can call each time ax[0, 0]
ax0, ax1 = ax.flatten()

# Preparing data for plotting smooth function
# Creating list with numpy method 'arange'
x0 = np.arange(1, 30, 0.1)
y0 = f_array(x0)

# Plotting smooth function in range [1, 30]
# Giving title to the figure and axises
ax0.set_title('Smooth function f(x)')
ax0.set_xlabel('x')
ax0.set_ylabel('y')
# Showing dashed lines to the found minimums
# By defining 'ymax' or 'xmax' we limit the end of the lines
# 'ymax' or 'xmax' can be in range between 0 and 1
ax0.axvline(x=4.1362, ymax=0.85, color='c', linestyle='--', linewidth=0.8)
ax0.axhline(y=1.7452, xmax=0.14, color='c', linestyle='--', linewidth=0.8)
ax0.axvline(x=25.8801, ymax=0.05, color='r', linestyle='--', linewidth=0.8)
ax0.axhline(y=-11.8988, xmax=0.83, color='r', linestyle='--', linewidth=0.8)
# Another way to draw the limited line is as following:
# ax0.plot(np.arange(1, 10, 2), np.array([2]*5))  # arrays fo axises 'x' and 'y' has to have the same dimensions
# Creating the figure for smooth function
ax0.plot(x0, y0, 'b', 4.1362, 1.7452, 'co', 25.8801, -11.8988, 'ro')

# Preparing data for plotting non-smooth function
# Creating list with numpy method 'arange'
x1 = np.arange(1, 30, 0.1)
y1 = h_array(x1)

# Plotting non-smooth function in range [1, 30]
# Giving title to the figure and axises
ax1.set_title('Non-smooth function h(int(f(x)))')
ax1.set_xlabel('x')
ax1.set_ylabel('y')
# Showing dashed lines to the found minimum
# By defining 'ymax' or 'xmax' we limit the end of the lines
# 'ymax' or 'xmax' can be in range between 0 and 1
ax1.axvline(x=25.0463, ymax=0.05, color='r', linestyle='--', linewidth=0.8)
ax1.axhline(y=-11, xmax=0.85, color='r', linestyle='--', linewidth=0.8)
ax1.axvline(x=30, ymax=0.43, color='c', linestyle='--', linewidth=0.8)
ax1.axhline(y=-5, xmax=0.95, color='c', linestyle='--', linewidth=0.8)
# Another way to draw the limited line is as following:
# ax0.plot(np.arange(1, 10, 2), np.array([2]*5))  # arrays fo axises 'x' and 'y' has to have the same dimensions
# Creating the figure for non-smooth function
ax1.plot(x1, y1, 'b', 30, -5, 'co', 25.0463, -11.0, 'ro')

# Function to make distance between figures
plt.tight_layout()
# Giving the name to the window with figures
figure.canvas.set_window_title('Smooth and non-smooth functions to be optimized')
# Showing result
plt.show()


import numpy as np
import scipy.stats as sps
from scipy.integrate import simps
def cdf(Data,ccdf = True):
    """ This function calculates only the cdf (or ccdf) of the data using the method described belwo in 'distirbution'. It does not interpolate. """
    DS = np.sort(Data)
    ECDF = np.linspace(0., 1., len(DS))
    if ccdf == True:
        ECDF = 1-ECDF
    return DS, ECDF

def get_hist(Data,N):
    """
    This function calculates the histogram of Data.
    N is the number of bins to separate the data into.
    returns:
        x: array of bin centers
        hist: histogram
    """
    hist, edges = np.histogram(Data,N,density = True)
    #We are interested in the middle points inside the bins, not the edges of the bins:
    bin_centers=(edges[:-1]+edges[1:])/2

    return bin_centers,hist

def distribution(Data,N,kernel=False,ccdf = True):
    """ This function calculates the pdf and ccdf of Data, either by histogram or by gaussian kernels.
    N:
    If histogram is used, N is the number of bins to separate the data into.
    If kernel is used, N gives the number of data points.
    ccdf: if true, returns the complementary cdf
    """
    if kernel==False:
        # Calculate PDF
        pdf, edges = np.histogram(Data,N,density = True)
        #We are interested in the middle points inside the bins, not the edges of the bins:
        bin_centers=(edges[:-1]+edges[1:])/2

        # Finding the CDF:
        # This sorts the data (with M datapoints) and, for each data point the cdf increases by 1/M from 0 to (M-1)/M
        # This is an unbiased estimator for the CDF
        # https://en.wikipedia.org/wiki/Empirical_distribution_function
        DS = np.sort(Data)
        ECDF = np.arange(len(DS))/float(len(DS))

        #We wish to use the bin_centers as data points, and interpolate:
        cdf = np.interp(bin_centers,DS,ECDF)

        if ccdf == True:
            cdf=1.0-cdf # We want the complementary cummulative distribution function

        return pdf, cdf, bin_centers
    elif kernel==True:
        X = np.linspace(min(Data),max(Data),N)

        pdf_func=sps.gaussian_kde(Data)
        pdf=pdf_func(X)

        cdf_func = lambda ary: np.array([pdf_func.integrate_box_1d(-np.inf, x) for x in ary])
        cdf=1-cdf_func(X)

        return pdf, cdf, X

def joint_pdf(X,Y,N=64, pdfs = False):
    ''' This function creates the joint PDF of the datasets X and Y. A square is created with N data points on each side.
    pdfs: if True, also returns the marginal PDFs from the joint PDF.
    '''

    H,xedges,yedges=np.histogram2d(X,Y,N,normed=True)
    #Use midpoints, not edges
    x = xedges[:-1] + 0.5 * (xedges[1:] - xedges[:-1])
    y = yedges[:-1] + 0.5 * (yedges[1:] - yedges[:-1])

    #xx,yy=np.meshgrid(x,y, indexing = 'ij')
    xx,yy=np.meshgrid(xedges,yedges,indexing = 'ij')
    print
    if pdfs == False:
        return H, xx, yy
    elif pdfs == True:
        Xpdf = simps(H,y,axis = 1)
        Xpdf = Xpdf/simps(Xpdf,x)
        Ypdf = simps(H,x,axis = 0)
        Ypdf = Ypdf/simps(Ypdf,y)
        return H,Xpdf,Ypdf,x,y,xx,yy


################### IMPORT ####################################################
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm, multivariate_normal, bernoulli
from statsmodels.discrete.discrete_model import Probit
from statsmodels.tsa.stattools import acf
from statsmodels.graphics.tsaplots import plot_acf
np.random.seed(0)

################### SIMULATED DATA ############################################
# simulated data
np.random.seed(0)
n = 200 # number of observations
k = 3 # number of explanatory variables

X = np.ones((n,k+1))
X[:,1:] = np.random.normal(loc=0,scale=1,size=k*n).reshape(n,k)
beta_true = np.matrix( [0.8,0.7,0.4,0.6]).T
sigma_true = 0.1
eta = np.dot(X,beta_true) + np.random.normal(loc=0, scale = sigma_true, size=(n,1))
Y =  bernoulli.rvs(p = norm.cdf(eta))

################### ALGORITHM #################################################

#Better perfomance to work in matrix form, avoid using loops
#PROOF: https://www.statlect.com/fundamentals-of-statistics/probit-model-maximum-likelihood
def probit_likelihood(beta, x, y):
    q = 2*y - 1
    eta = np.dot(x,beta)
    eta_q = np.multiply(q.T,eta)
    p = norm.cdf(eta_q)
    likehood = np.prod(p)
    return likehood

def beta_prior(beta, lambda0=10):
    mean = np.zeros(shape=(beta.shape[0],))
    cov = (1/lambda0) * np.identity(beta.shape[0])
    prior_distribution = multivariate_normal(mean=mean, cov=cov)
    return prior_distribution.pdf(beta)

def posterior(beta, x, y, lambda0=10):
    return beta_prior(beta, lambda0)*probit_likelihood(beta, x, y)

#Work in log form for numeric stability
def acceptance_probability(beta_new, beta_old, x, y, lambda0=10):
    posterior_new = posterior(beta_new, x, y, lambda0)
    posterior_old = posterior(beta_old, x, y, lambda0)
    logratio = np.log(posterior_new) - np.log(posterior_old)
    ratio = np.exp(logratio)
    return min(1, ratio)

#Fisher information for bernoulli Y with probit link
## Demidenko(2001) ("Computational aspects of probit model") & Graziani slides (Lecture 5, slide 7)
def Fisher_information_probit(beta,x):
    np.seterr(divide = 'ignore') ## avoid error while divide
    n,p = x.shape
    eta = np.dot(x,beta)
    numerator = norm.pdf(eta)**2
    denominator = (norm.cdf(eta))*(1.0001-norm.cdf(eta))
    value = np.divide(numerator, denominator)
    ## Set 1 when the value is 'inf', 1 = neutral multiplier
    ##value[value==np.inf] = 1
    W = value * np.identity(n)
    I = x.T @ W @ x
    invI = np.linalg.inv(I)
    return invI

### Algorithm inverse fisher variance
def metropolsH_randomWalk_fisher(Y,X,lambda0 = 10, interations=5000, start=0, tau=1):
    n,p = X.shape
    np.random.seed(0)
    beta_tried = np.zeros(shape=(p,interations))
    beta_out = np.zeros(shape=(p,interations))
    beta_old = np.random.multivariate_normal(mean=start * np.ones((p,)), cov=np.identity((p))/lambda0)
    acpt = 0
    for i in range(interations):
      covariance = Fisher_information_probit(beta_old,X)
      beta_new = np.random.multivariate_normal(mean=beta_old, cov= tau * covariance) #either simulate a multiv normal with mean = beta or sum beta_old with mean zero
      beta_tried[:,i] = beta_new.T
      u = np.random.rand()
      alpha = acceptance_probability(beta_new, beta_old, X, Y, lambda0)
      if u < alpha:
          beta_old = beta_new
          acpt+=1
      beta_out[:,i] = beta_old.T
    acpt_rate = acpt/interations
    return beta_out, beta_tried, acpt_rate

################### RUN THE MODEL #############################################
interations = 10000
### Change tau and lambda0 to check the better value -- Tau matter more than lambda0
MLE = np.array([0.7017, 0.6756, 0.4959, 0.4551])
### Start=0 also give us very good results
beta, beta_tried, acpt_rate = metropolsH_randomWalk_fisher(Y=Y,X=X, interations=interations, tau=0.7, lambda0=1, start=MLE)
print(f"The acceptance rate was {np.round(acpt_rate*100,4)}% \n")
for n in range(beta.T[0].shape[0]):
    true = float(beta_true[n])
    m = np.around(np.mean(beta[n,:]),4)
    stdev = np.around(np.std(beta[n,:]),4)
    print(f"Beta({n}) mean prediction: {m}, 95% CI: ({round(m-1.96*stdev,4)},{round(m+1.96*stdev,4)}), true_beta: {true} \n")

################### DIAGNOSTICS ###############################################

#Plots
### Graphic Markov Chain
for n in range(beta.shape[0]):
    print(f"\n BETA({n}) \n")
    plt.figure(figsize=(12,8))
    plt.title(f'Beta ({n})')
    plt.plot(beta[n,100:].T, color="black")
    plt.axhline(beta_true[n], color='r', linestyle='-')
    plt.xlabel('Iterations', fontsize=12)
    plt.ylabel(f'Beta ({n})', fontsize=12)
    plt.legend(['Markov Chain','True Value'], fontsize=12, loc=1)
    plt.show()

### Histogram Markov Chain
for n in range(beta.shape[0]):
    print(f"\n BETA({n}) \n")
    plt.figure(figsize=(12,8))
    plt.title(f'Beta ({n})')
    plt.hist(beta[n,100:], bins=25, color="gray")
    plt.axvline(beta_true[n], color='r', linestyle='-')
    plt.axvline(np.mean(beta[n,:]), color='green', linestyle='-')
    plt.axvline(MLE[n], color='blue', linestyle='-')
    plt.xlabel('Density', fontsize=12)
    plt.ylabel(f'Beta ({n})', fontsize=12)
    plt.legend(['True value', 'Markov mean', 'MLE','Density'], fontsize=12, loc=1)
    plt.show()

### Convergence to true value
for n in range(beta.shape[0]):
    beta_avg_cum = np.zeros(shape=(interations,))
    for t in range (interations):
        beta_avg_cum[t] = np.sum(beta[n,:t+1])/(t+1)
    plt.figure(figsize=(12,8))
    plt.title(f'Beta ({n})')
    plt.plot(beta_avg_cum.T, color="black")
    plt.axhline(beta_true[n], color='r', linestyle='-')
    plt.xlabel('Interations', fontsize=12)
    plt.ylabel(f'Beta ({n})', fontsize=12)
    plt.ylim(0.25,1)
    plt.legend(['Cumulative Average','True value'], fontsize=12, loc=1)
    plt.show()

## All Graphics toghether
for n in range(beta.shape[0]):
    beta_avg_cum = np.zeros(shape=(interations,))
    for t in range (interations):
        beta_avg_cum[t] = np.sum(beta[n,:t+1])/(t+1)
    plt.figure(figsize=(12,8))
    plt.title(f'Beta ({n})')
    plt.plot(beta[n,:].T, color="black")
    plt.plot(beta_avg_cum.T, color="blue")
    plt.axhline(beta_true[n], color='r', linestyle='-')
    plt.xlabel('Interations', fontsize=12)
    plt.ylabel(f'Beta ({n})', fontsize=12)
    plt.legend(['Markov Chain','Cumulative average','True value'], fontsize=12, loc=1)
    plt.show()

### Graphic to check beta tried and beta markov chain
for n in range(beta.shape[0]):
    bot = 5000
    up = 6000
    r = [i for i in range(up - bot)]
    print(f"\n BETA({n}) \n")
    plt.figure(figsize=(12,8))
    plt.title(f'Beta ({n})')
    plt.plot(beta[n,bot:up].T, color="black")
    plt.scatter( r, beta_tried[n,bot:up].T, color="red", marker='*')
    plt.axhline(beta_true[n], color='blue', linestyle='-')
    plt.xlabel('Interations', fontsize=12)
    plt.ylabel(f'Beta ({n})', fontsize=12)
    plt.legend(['Markov Chain', 'True value', 'Beta suggested'], fontsize=12, loc=1)
    plt.show()

#ACF values
for n in range(beta.shape[0]):
    burn_in = 5000
    acfunc = acf(beta[n,burn_in:].T, nlags=10, fft=False)
    print(f"The ACF matrix for Beta{n} is {acfunc}\n")

#ACF plots
for n in range(beta.shape[0]):
    plt.rc("figure", figsize=(12,8))
    burn_in = 5000
    print(f"\n The ACF plot for Beta{n} is: \n")
    plot_acf(beta[n,burn_in:].T, lags=30, title = f'Auotocorrelation Beta {n}')
    plt.show()

#ACF plots -5 in five
for n in range(beta.shape[0]):
    plt.rc("figure", figsize=(12,8))
    burn_in = 5000
    print(f"\n The ACF plot for Beta{n} is: \n")
    plot_acf(beta[n,burn_in::5].T, lags=20, title = f'Auotocorrelation Beta {n} - Blocks of 5')
    plt.show()

################### BULT-IN  ##################################################
## MLE estimator
model = Probit(Y, X.astype(float))
probit_model = model.fit()
print("MLE estimate: \n")
print(probit_model.summary())

import numpy,from scipy,from statsmodels


#!/bin/usr python3

import numpy as np

# 数値微分
def numerical_diff(f, x):
    h = le - 4
    return (f(x + h) - f(x - h) / (2 * h))

# 偏微分の場合は片方の関数の値を固定して上記の数値微分の関数に流すことで算出することが可能
def function2(x):
    return x[0] ** 2 + x[1] ** 2
# 上記のような変数が２つある関数に対しては偏微分を使用して算出する

# 勾配
# 微分をすることで勾配を算出する
def numerical_gradient(f, x):
    # 0で除算することのないように小さな値を設定しておく
    h = le - 4
    # 0の配列を生成する（xと同じ構造）
    grad = np.zeros_like(x)
    for idx in range(x.size):
        tmp_val = x[idx]
        # f(x+h)の計算
        x[idx] = tmp_val + h
        fxh1 = f(x)

        # f(x-h)の計算
        x[idx] = tmp_val -h
        fxh2 = f(x)

        grad[idx] = (fxh1 - fxh2) / (2 * h)
        # 値をもとに戻しておく
        x[idx] = tmp_val
        return grad

# 勾配降下法
# 勾配の値をxから引くことで徐々に勾配の値を0に近づけるようにする
# lrは学習係数である
def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x
    for i in range(step_num):
        grad = numerical_gradient(f, x)
        x -= lr * grad
    return x

# ニューラルネットワークでは重みパラメータに対する損失関数の勾配を算出し、損失関数の値を小さくすることで最適な重みパラメータの値を学習させる

# 余談
# かんたんな関数についてはlambda記法を使用して関数を生成することも可能
f = lambda x, y: x+y
print(f(1,2)) # 3と出力


import numpy as np

class KMeans:

    def __init__(self, k, c=None, tol=1e-3, max_iter=10000):
        assert k > 1, f"k should be greater than 1, but now is {k}"
        self._k = k
        if c:
            assert k == len(
                c
            ), f"The number of given centers should be equal to k, but {k} != {len(c)}"
            self._centers = c
        else:
            self._centers = None
        self._tol = tol
        self._max_iter = max_iter

    @property
    def k(self):
        return self._k

    @property
    def centers(self):
        return self._centers

    def fit(self, x):
        if not self._centers:
            self._centers = self._init_center(x, self._k)
        self._centers, labels, best_iter = self._kmeans(x, self._centers,
                                                        self._tol,
                                                        self._max_iter)
        return self._centers, labels, best_iter

    def predict(self, x):
        dist_matrix = np.zeros((len(x), len(self._centers)))
        for idx, center in enumerate(self._centers):
            dist_matrix[:, idx] = self._compute_dist(x, center)
        labels = np.argmin(dist_matrix, axis=1)
        return labels

    def _init_center(self, x, n=2):
        centers = list()
        indexes = list()
        indexes.append(np.random.randint(len(x)))
        center_1 = x[indexes[-1]]
        centers.append(center_1)
        dist_array = self._compute_dist(x, center_1).tolist()
        indexes.append(np.argmax(dist_array))
        centers.append(x[indexes[-1]])
        if n > 2:
            for _ in range(2, n):
                dist_array = np.zeros(len(x))
                for center in centers:
                    dist_array += self._compute_dist(x, center)
                idx = np.argmax(dist_array)
                while (idx in indexes):
                    dist_array[idx] = 0
                    idx = np.argmax(dist_array)
                indexes.append(idx)
                centers.append(x[indexes[-1]])
        return centers

    def _compute_dist(self, x, center):
        return np.sqrt(np.sum(np.square(x - center), axis=1))

    def _l2_norm(self, x):
        x = np.ravel(x, order='K')
        return np.dot(x, x)

    def _kmeans(self, x, centers, tol=1e-3, max_iter=10000):
        labels = np.zeros(len(x))
        n_iter = 1
        for i in range(max_iter):
            dist_matrix = np.zeros((len(x), len(centers)))
            for idx, center in enumerate(centers):
                dist_matrix[:, idx] = self._compute_dist(x, center)
            labels = np.argmin(dist_matrix, axis=1)
            new_centers = np.zeros_like(centers)
            for idx in range(len(centers)):
                new_centers[idx] = np.mean(x[labels == idx], axis=0)
            shifts = self._l2_norm(new_centers - centers)
            if shifts <= tol:
                break
            else:
                centers = new_centers
                n_iter = i + 1

        return centers, labels, n_iter


''' singular_value_decomposition_study
This module contains functions relevant to the study of the error of
matrix vector multiplications when the matrix is approximated by the singular value decomposition.

---
Functions
singular_value_decomposition_error_study(m: int, n: int, k: int, *, order: np.array = None, H: np.matrix = None) -> None

This function provides the intimate working details of evaluating the difference between the analytical solution
of a matrix vector multiplication Hx. Where H is fixed and x is generated randomly.

two_norm_of_matrix(a_mat: np.matrix) -> bool:

This function evaluates Hx = \lambda x when x is an eigenvector of H^{h}H. I used this to verify that my basic
linear algebra code used to multiply vectors by matrices wasn't wrong in some very fundamental way.

singular_value_decomposition_experiment() -> None:

This function provides the operating parameters for executing the singular_value_decomposition_error_study.
As well as plotting the outputs.
---
Author: Kevin Exton
Date: 2020-11-04
'''
from collections import namedtuple

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

def singular_value_decomposition_error_study(m: int, n: int, k: int, *, order: np.array = None, H: np.matrix = None) -> None:
    '''
    Parameters:
    ---
    m: int, the number of rows in the matrix H
    n: int, the number of columns in the matrix H
    k: int, the number of iterations of H*v to simulate
    order: np.array, the order of singular values to create the low rank approximation of matrix H
    H: np.matrix, the matrix to approximate.

    ---
    Returns:
    ---
    None

    ---
    Example:
    ---
    singular_value_decomposition_error_study(100, 60, 10000)

    ---
    Description:
    ---

    A random matrix H is defined. If H is not supplied
    k random vectors v are defined

    Perform the singular value decomposition of H.

    Create a low rank approximation of H, H_low_rank.

    The low rank approximation is built in the order of largest to smallest singular values
    unless an order is explicitly provided.

    Find the upper bound of the 2-norm error between H and H_low_rank.
    Defined in the text 'Linear Algebra - Liesen', also in my obsidian notes
    under 'Singular Value Decomposition'.

    Evaluate the actual error between the analytical solution Hv and the approximate solution
    H_low_rank * v. Using the following method.

    ||H * v - H_low_rank * v||/||v||

    A comparison is made between the theoretical upper bound of the error imposed by the low rank
    approximation of the singular value decomposition and the expected error from selecting vectors v
    at random.
    ---
    Author: Kevin Exton
    Date: 2020-10-21

    '''
    # Error Checking
    if order is not None and len(order) != n:
        print(f'the number of elements in order should be {n}.')
        return None

    if H is not None:
        if H.shape[0] > m or H.shape[1] > n:
            print(f'the dimensions of H need to be {m} x {n}.')
            return None

    # define H and v.

    if H is None:
        H = (10*np.random.random((m,n))-5*np.ones((m,n))) + (10j*np.random.random((m,n))-5j*np.ones((m,n)))

    # and v is a random vector of the appropriate dimensions.
    # Actually if I define v as a massive matrix and then iterate through the columns that
    # would give me K iterations to look at min,max and average error.

    # K = 10000

    v = (10*np.random.random((n,k))-5*np.ones((n,k))) + (10j*np.random.random((n,k))-5j*np.ones((n,k)))

    # Now I need the singular value decomposition of H
    U, s, Vh = linalg.svd(H)

    # And I can create a low rank approximation of H
    # I can loop through all the ranks of a full rank matrix and create a list of error statistics

    # First create the list of error statistics it will be a K list of named tuples
    ErrorStatistic = namedtuple('ErrorStatistic', 'error_bound min_error mean_error max_error')
    error_statistics = []

    # Create a variable to choose the order of the singular values to construct the
    # low rank approximation
    # order = np.random.choice(np.arange(n), n, replace=False)
    if order is None:
        order = np.arange(n)

    for rank in range(1,n+1):
        selection = order[0:rank]
        # print(np.matrix([U[:,i] for i in selection]).T.shape)

        # Then constructing the matrix as U_low_rank \in C^{n,10} times Sigma_low_rank \in R^{10,10} times Vh_low_rank \in C^{10,m}
        H_low_rank = np.matmul(
            np.matmul(
                np.matrix([U[:,i] for i in selection]).T,
                linalg.diagsvd([s[i] for i in order],U.shape[0],Vh.shape[0])[0:rank,0:rank]
            ), np.matrix([Vh[i,:] for i in selection])
        )
        # The error in the 2 norm sense is going to be ||H*v - H_low_rank*v||
        # And the theoretical upper bound (I think) of that error is going to be the largest excluded singular value
        # First we find that singular value

        error_bound = max( s[i] for i in (set(order) - set(selection)) ) if rank < len(s) else 0
        # error_bound = max(set(s) - set(s[i] for i in order)) if rank < len(s) else 0

        # error_bound = s[rank] if rank < len(s) else 0 # It's RANK not RANK+1 because of python's 0 indexing
        # print(f'Error Bound is: {error_bound}')

        # Now evaluate the error directly normalised by the magnitude of v ||H*v - H_low_rank*v||/||v||
        # (Wait is the norm a linear process can I do this?)

        # iterate through the loop K times and find the min, max and average.
        error_vec = [np.linalg.norm(np.matmul(H,v[:,i]) - np.matmul(H_low_rank, v[:,i]), ord=2)/np.linalg.norm(v[:,i], ord=2) for i in range(k)]
        error_statistics.append(ErrorStatistic(error_bound, min(error_vec), sum(error_vec)/k, max(error_vec)))

    # I'm curious about what the distribution looks like. So I'm going to run a single error vec iteration. with a fixed rank estimation
    # This will be repeating one of the iterations in the above loop which adds redundancy, but I don't think it's a big deal here.
    rank = 3
    selection = order[0:rank]
    H_low_rank = np.matmul(
        np.matmul(
            np.matrix([U[:,i] for i in selection]).T,
            linalg.diagsvd([s[i] for i in order],U.shape[0],Vh.shape[0])[0:rank,0:rank]
        ), np.matrix([Vh[i,:] for i in selection])
    )
    error_vec = [np.linalg.norm(np.matmul(H,v[:,i]) - np.matmul(H_low_rank, v[:,i]), ord=2)/np.linalg.norm(v[:,i], ord=2) for i in range(k)]

    # Below I'm going to try and plot this error_statistics madness
    fig, axs = plt.subplots(2,1) # Create a figure containing a single axes.
    axs[0].plot(range(1,n+1),[error_statistics[i].error_bound for i in range(n)])
    axs[0].errorbar(
        range(1,n+1),
        [error_statistics[i].mean_error for i in range(n)],
        [
            [error_statistics[i].mean_error - error_statistics[i].min_error for i in range(n)],
            [error_statistics[i].max_error - error_statistics[i].mean_error for i in range(n)]
        ],
        fmt='or',
        capsize=3)
    # Add axs labels
    axs[0].set_ylabel('Error Magnitude in the 2-norm')
    axs[0].set_xlabel('Rank of SVD Approximation')
    axs[0].set_title('Error Bounds Comparison between upper bound and \'real\' error.')

    # Plot the Histogram
    axs[1].hist(error_vec, bins=1000)

    # Add axs labels
    axs[1].set_ylabel('No. of Vectors')
    axs[1].set_xlabel(f'Error Magnitude in the 2-norm of rank {rank} approximation')
    axs[1].set_title(f'Error Magnitude Distribution in the 2-norm of rank {rank} approximation')

    # Label the figure
    fig.suptitle('A Study of the Error Performance of the Singular Value Decomposition in the 2-norm')
    plt.show()

def two_norm_of_matrix(a_mat: np.matrix) -> bool:
    """
    Parameters:
    ---

    a_mat: np.matrix

    ---
    Returns:
    ---
    boolean

    ---
    Example:
    ---
    x = two_norm_of_matrix(a_mat)

    ---
    Description:
    ---
    From the definition of the SVD as I've outlined in my note [2].
    There is a property of tall matrices that falls out.

    ||Ax||_{2}^{2} = \lambda ||x||_{2}^{2}

    A nice proof is outlined in [1] but just for future reference
    (in case math stackexchange disappears forever) I'll write it out again here.

    Begin Quote:

    From the SVD of A = UDV^{T} we can see that the eigenvalues
    of A^{T}A = VD^{2}V^{T} are just squared ones from A. At the same time
    the columns of V are the eigenvectors of A^{T}A. So, exploiting orthogonality of
    these eigenvectors.

    ||Ax||_{2}^{2} = ||UDVx||_{2}^{2} = ||D(Vx)||_{2}^{2} = ||De_{\lambda}||x||||_{2}^{2} = ||\sqrt{\lambda}||x||||_{2}^{2} = \lambda ||x||_{2}^{2}

    My calculation is slightly different from this definition because I don't square these values.
    This doesn't really make a big difference for the exercise below because
    the 2-norm is positive definite, so negative numbers are not a problem.

    End Quote
    ---
    References:
    ---
    [1] https://math.stackexchange.com/questions/2165249/2-norm-of-a-matrix
    [2] Obsidian Note: Singular Value Decomposition
    ---
    Author: Kevin Exton
    Date: 2020-10-16
    """
    a_mat_2 = np.matmul(np.asmatrix(a_mat).H, np.asmatrix(a_mat))
    eigval, eigvec = np.linalg.eigh(a_mat_2)
    x = np.asmatrix(eigvec[:,0])

    y = np.matmul(a_mat, x)
    norm_y = np.linalg.norm(y)

    norm_x = np.sqrt(eigval[0])*np.linalg.norm(x)

    return np.allclose(norm_y, norm_x, rtol=1e-5)

def singular_value_decomposition_experiment() -> None:
    # a = np.random.randint(1,10,size=(4,2)) + 1j*np.random.randint(1,10,size=(4,2))
    # print(two_norm_of_matrix(np.asmatrix(a)))
    m = 60
    n = 30

    H = (10*np.random.random((m,n))-5*np.ones((m,n))) + (10j*np.random.random((m,n))-5j*np.ones((m,n)))

    # Worst Case Scenario
    singular_value_decomposition_error_study(
        m,
        n,
        10000,
        order = np.arange(n)[::-1],
        H = H
    )

    # Random Case Scenario
    singular_value_decomposition_error_study(
        m,
        n,
        10000,
        order = np.random.choice(np.arange(n), n, replace = False),
        H = H
    )

    # Best Case Scenario
    singular_value_decomposition_error_study(
        m,
        n,
        10000,
        order = np.arange(n),
        H = H
    )


from typing import Callable
import numpy as np

def newtonSolver(f: Callable, f_prime: Callable, guess: float,
                 tol: float=10e-6, prev: float=0) -> float:
    """Newton method solver for 1 dimension, implemented recursively.

    Arguments:
        f {Callable} -- Objective function (must have zero root).
        f_prime {Callable} -- First derivative of objective with respect to
                              the decision variable.
        guess {float} -- Guess for the decision variable.

    Keyword Arguments:
        tol {float} -- Tolerance level (default: {10e-6}).
        prev {float} -- Guess from previous iteration (for convergence check).

    Returns:
        float -- Solution to the function s.t. f(x) = 0.
    """

    # Assigning current guess to x_old
    x_old = guess

    # Checking if decision variable changed by less than tolerance level
    if np.abs(x_old - prev) < tol:
        return x_old
    else:
        # Compute new estimate for x
        x_new = x_old - (f(x_old) / f_prime(x_old))
        return newtonSolver(f=f, f_prime=f_prime, guess=x_new,
                            tol=tol, prev=x_old)


from itertools import product

from numpy import einsum, zeros
from objects.grtensors.christoffelsymbol import ChristoffelSymbol
from objects.simplifyobjects import Simplify
from sympy import Array, MutableDenseNDimArray, diff

def derivative_of_chris(coord_sys, chris_symb, i, j, k, l):
    """
    Taking the partial derivative of a given christoffel symbol [Gamma^j_kl] with respect to the
    [i]'th component

    Args:
        coord_sys [list]: The coordinate system given as a list (e.g., [t,x,y,z])
        chris_symb [sympy.tensor]: Given christoffel symbol, Gamma^j_kl
        i,j,k,l [int]: Coordinate indices; (0-ndim)
    """
    return Simplify(diff(chris_symb[j, k, l], coord_sys[i]))

class RiemannTensor(ChristoffelSymbol):
    def __init__(self, metric_tensor, coord_sys):
        """
        Creating the riemann tensor object

        Args:
            metric_tensor [list]: The metric tensor, provided by the user
            coord_sys [list]: The coordinate system given as a list (e.g., [t,x,y,z])

        Returns:
            self.riemann_type [str]: Type of the riemann tensor. Default type is 'uddd'
            self.riemann_obj [sympy.tensor]: The riemann tensor, R^l_ijk
        """
        ChristoffelSymbol.__init__(self, metric_tensor, coord_sys)
        self.riemann_type = 'uddd'
        riemann_tensor = MutableDenseNDimArray(zeros((self.ndim,)*4))
        for l, i, j, k in product(range(self.ndim), repeat=4):
            Q1 = derivative_of_chris(
                self.coord_sys, self.chris_obj, j, l, i, k)
            Q2 = derivative_of_chris(
                self.coord_sys, self.chris_obj, i, l, j, k)
            einstein_sum = 0
            for p in range(self.ndim):
                I1 = self.chris_obj[p, i, k] * self.chris_obj[l, j, p]
                I2 = self.chris_obj[p, j, k] * self.chris_obj[l, i, p]
                einstein_sum += (I1 - I2)
            riemann_tensor[l, i, j, k] = Q1 - Q2 + einstein_sum
        self.riemann_obj = riemann_tensor

    def get_riemanntensor(self):
        """
        Returns the riemann tensor object
        """
        return Simplify(self.riemann_obj)

    def get_riemanntensor_type(self):
        """
        Returns the type of the riemann tensor
        """
        return self.riemann_type

    def lower_index(self, xriemann_tensor):
        """
        Lowering the index of the riemann tensor

        Args:
            xriemann_tensor [sympy.tensor]: Given riemann tensor
        """
        return Array(einsum('abcd,ak->kbcd', xriemann_tensor, self.metric_obj, optimize='optimal'))

    def raise_index(self, xriemann_tensor):
        """
        Raising the first index of the riemann tensor

        Args:
            xriemann_tensor [sympy.tensor]: Given riemann tensor
        """
        return Array(einsum('abcd,bk->akcd', xriemann_tensor, self.inverse_metric_obj, optimize='optimal'))

    def raise_index1(self, xriemann_tensor):
        """
        Raising the second index of the riemann tensor

        Args:
            xriemann_tensor [sympy.tensor]: Given riemann tensor
        """
        return Array(einsum('akcd,cl->akld', xriemann_tensor, self.inverse_metric_obj, optimize='optimal'))

    def raise_index2(self, xriemann_tensor):
        """
        Raising the third index of the riemann tensor

        Args:
            xriemann_tensor [sympy.tensor]: Given riemann tensor
        """
        return Array(einsum('akld,df->aklf', xriemann_tensor, self.inverse_metric_obj, optimize='optimal'))

    def vary_riemanntensor_type(self, xriemann_tensor, new_type):
        """
        Varying the type of the riemann tensor

        Args:
            xriemann_tensor [sympy.tensor]: Given riemann tensor
            new_type [str]: The new type of the riemann tensor. It should be given
            in terms of 'u': contravariant (upper-indices) and 'd': covariant (lower-indices)

        Returns:
            The new riemann tensor for a given type
        """
        self.riemann_type = new_type
        if new_type == 'dddd':
            return Simplify(self.lower_index(xriemann_tensor))
        elif new_type == 'uddd':
            return Simplify(self.riemann_obj)
        elif new_type == 'uudd':
            return Simplify(self.raise_index(xriemann_tensor))
        elif new_type == 'uuud':
            return Simplify(self.raise_index1(self.raise_index(xriemann_tensor)))
        elif new_type == 'uuuu':
            return Simplify(self.raise_index2(self.raise_index1(self.raise_index(xriemann_tensor))))


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

# ### Generate Samples from the Posterior
#
# The predictive posterior for Gaussian Process Regression is given by:
#
# $$ \boldsymbol{f_{*}} | X, \boldsymbol{f}, X_{*} \sim \mathcal{N} ( \: K(X_{*}, X) K(X,X)^{-1} \boldsymbol{f}, \; K(X_{*}, X_{*} ) - K(X_*, X) K(X,X)^{-1}K(X,X_*) \: )$$
#
#  where $X_{*}$ contains the input points as columns. The matrix K is computed by using the squared exponential kernel function $ k(\boldsymbol{x_p}, \boldsymbol{x_q} ) = exp(-\frac{1}{2} \left| \boldsymbol{x_p} - \boldsymbol{x_q} \right|^2 ) $ for each combination of input points $ a_{ij} = k(x_i, x_j) $.
#
# According to A.2, to sample from a $ x \sim \mathcal{N}(\boldsymbol{m},K) $ with arbitrary mean $\boldsymbol{m}$ and covariance matrix K using a scalar Gaussian generator, we use the follow procedure:
#
# 1. Compute Cholesk of K i.e. $K = LL^{\intercal}$
# 2. Generate $\boldsymbol{u} \sim \mathcal{N}(\boldsymbol{0}, I)$ by separate calls to scalar Gaussian generator
# 3. $\boldsymbol{x} = \boldsymbol{m} + L \boldsymbol{u} $

import numpy as np
import matplotlib.pyplot as plt

def sq_exp(x,y,length=1.0):
    "x,y: numpy arrays"
    return np.exp (-0.5 * np.linalg.norm( (x-y)/length, ord = 2) ** 2 )

def generateSPD_fromKDE(num_test):
    """Generate a SPD matrix using the KDE method.

    @num_test: the shape of the SPD, namely, num_test = row(K) = col(K)

    @ret: a SPD matrix using the KDE method
    """
    x_min = -5
    x_max = 5

    num_train = 10
    epsilon = 1e-6
    l = 1.0

    # evenly spaced training and test points
    training_points = np.linspace(x_min, x_max, num_train)
    test_points = np.linspace(x_min, x_max, num_test)
    function_values = np.sin(training_points)

    # submatrices

    K_x_star_x_star = np.zeros( (num_test, num_test) )

    for i, x_star_1 in enumerate(test_points):
        for j, x_star_2 in enumerate(test_points):
            K_x_star_x_star[i,j] = sq_exp( np.array([x_star_1]), np.array([x_star_2]), l)

    K_x_star_x = np.zeros( (num_test, num_train) )

    for i, x_star in enumerate(test_points):
        for j, x in enumerate(training_points):
            K_x_star_x[i, j] = sq_exp(np.array([x_star]), np.array([x]), l)

    K_x_x = np.zeros(( num_train, num_train))

    for i, x in enumerate(training_points):
        for j, y in enumerate(training_points):
            K_x_x[i, j] = sq_exp(np.array([x]), np.array([y]), l)

    K_x_x_star = np.zeros((num_train, num_test))

    for i, x in enumerate(training_points):
        for j, x_star in enumerate(test_points):
            K_x_x_star[i, j] = sq_exp(np.array([x]), np.array([x_star]), l)

    part_mean = np.dot(K_x_star_x, np.linalg.inv(K_x_x + epsilon * np.eye(num_train) ) )
    K = K_x_star_x_star - np.dot( part_mean, (K_x_x_star) ) + epsilon * np.eye(num_test)

    return K

# # sample from the posterior distribution

# # 1 cholesky
# chol = np.linalg.cholesky(K)

# # 2 separate samples from gaussian(0,1)
# samples_1 = np.random.standard_normal((num_test,1))
# samples_2 = np.random.standard_normal((num_test,1))

# # 3 compute regression output
# m = np.dot(part_mean, function_values[:, np.newaxis])

# m_1 = (m + np.dot(chol, samples_1)).reshape(-1)
# m_2 = (m + np.dot(chol, samples_2)).reshape(-1)

# plt.plot(test_points, m_1, color="red", label="test sample 1")
# plt.plot(test_points, m_2, color="blue", label="test sample 2")
# plt.scatter(training_points, function_values, color="green", label="training")
# plt.fill_between(test_points, (m_1 - 2 * np.sqrt( np.diag(K) ) ), (m_1 + 2 * np.sqrt (np.diag(K) ) ), alpha=0.5, color="blue", label="95% interval" )
# plt.legend()
# plt.tight_layout()


import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn import metrics
import random
from sys import argv

number_of_iteration = 100000

# Load data
df = pd.read_csv('./data/data_hw1.csv')
df = df.sort_values(by=['R'], ascending=False)
# print(df.head(5))
# print(df.describe())

# Create and Reshape input and output vectors
X = df['R'].values.reshape(-1, 1)
y = df['T'].values.reshape(-1, 1)

# Split data for 90% train and 10% test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=0)

def bgd(beta_0, beta_1, x, y, alpha=0.0001):
    new_beta_0 = beta_0 + sum(sum(alpha * (y - beta_0 - (beta_1 * x))))
    new_beta_1 = beta_1 + sum(sum(alpha * x * (y - beta_0 - (beta_1 * x))))
    return new_beta_0, new_beta_1

def sgd(beta_0, beta_1, x, y, alpha=0.001):
    new_beta_0 = beta_0 + alpha * (y - beta_0 - (beta_1 * x))
    new_beta_1 = beta_1 + alpha * x * (y - beta_0 - (beta_1 * x))
    return new_beta_0, new_beta_1

if __name__ == '__main__':
    beta_0 = random.random()
    beta_1 = random.random()

    if argv[1] == 'bgd':
        for i in range(number_of_iteration):
            lr = 0.0001
            beta_0, beta_1 = bgd(beta_0, beta_1, X_train, y_train,lr)
            # print(beta_0, beta_1)
    elif argv[1] == 'sgd':
        for i in range(number_of_iteration):
            lr = 0.001
            rand = np.random.randint(0, len(X_train))
            beta_0, beta_1 = sgd(beta_0, beta_1, sum(X_train[rand]), sum(y_train[rand]), lr)
            # print(beta_0, beta_1)
    else:
        print('Unknown algorithm')
        exit()

    # Show output line

    # Generate input
    x = np.arange(start=10, stop=30)

    # Plot initial data
    plt.scatter(df['R'], df['T'])
    plt.ylabel('Time of recurrence')
    plt.xlabel('Cell size')

    # Plot prediction line
    plt.plot(x, (beta_0 + beta_1 * x).reshape(-1), 'r-')

    # Prettify
    # Regression equations.
    plt.text(21.5, 18, 'y={:.2f}+{:.2f}*x'.format(float(beta_0), float(beta_1)), color='red', size=12)

    # Calculate error
    y_pred = beta_0 + beta_1 * X_test
    print('We used {} algorithm for {} iterations with learning rate of {}'.format(argv[1], number_of_iteration, lr))
    print('Mean Absolute Error:', metrics.mean_absolute_error(y_test, y_pred))
    print('Mean Squared Error:', metrics.mean_squared_error(y_test, y_pred))
    print('Root Mean Squared Error:', np.sqrt(metrics.mean_squared_error(y_test, y_pred)))
    with open('./stat_{}.txt'.format(argv[1]), 'a') as f:
        f.write(str(metrics.mean_absolute_error(y_test, y_pred))
                + ', ' + str(metrics.mean_squared_error(y_test, y_pred))
                + ', ' + str(np.sqrt(metrics.mean_squared_error(y_test, y_pred)))
                + '\n')

    # Save and show it all
    plt.savefig('./plot/{}_output.png'.format(argv[1]))
    plt.show()


# Native

# Site-packages
import numpy as np

# Locals

class Regularizer:
    def __init__(self):
        pass

    def __call__(self):
        raise NotImplementedError()

    def grad(self):
        raise NotImplementedError()

class Placeholder(Regularizer):
    def __call__(self, X: np.array) -> float:
        return 0

    def grad(self, X: np.array) -> np.array:
        return np.zeros(X.shape[0])

class L1(Regularizer):
    def __init__(self, lambda_: float):
        self.lambda_ = lambda_

    def __call__(self, X: np.array) -> float:
        X_norm = np.abs(X).sum()
        return self.lambda_ * X_norm

    def grad(self, X: np.array) -> np.array:
        return self.lambda_ * np.sign(X)

class L2(Regularizer):
    def __init__(self, omega_: float):
        self.omega_ = omega_

    def __call__(self, X: np.array) -> float:
        X_norm_squared = np.linalg.norm(X) ** 2
        return self.omega_ * X_norm_squared

    def grad(self, X: np.array) -> np.array:
        return 2 * self.omega_ * X

class ElasticNet(Regularizer):
    def __init__(self, lambda_1: float, lambda_2: float):
        self.l1 = L1(1)
        self.l2 = L2(1)
        self.alpha = lambda_2 / (lambda_1 + lambda_2)

    def __call__(self, X: np.array) -> float:
        return self.alpha * self.l2(X) + (1 - self.alpha) * self.l1(X)

    def grad(self, X: np.array) -> np.array:
        return self.alpha * self.l2.grad(X) + (1 - self.alpha) * self.l1.grad(X)


import numpy as np
from scipy.linalg import toeplitz

class differentation:

    def __init__(self, N):
        self.ik = self.waves_coeff(N)
        self.diff1 = self.D1(N)
        self.diff2 = self.D2(N)

    @staticmethod
    def waves_coeff(N):
        k = np.zeros(N, dtype=complex)
        k[:] = np.fft.fftfreq(N) * N

        return 1j * k

    @staticmethod
    def chebfft(v):
        '''Chebyshev differentiation via fft.
           Ref.: Trefethen's 'Spectral Methods in MATLAB' book.
        '''
        N = len(v) - 1
        if N == 0:
            w = 0.0  # only when N is even!
            return w
        x = np.cos(np.pi * np.arange(0, N + 1) / N)
        ii = np.arange(0, N)
        V = np.flipud(v[1:N])
        V = list(v) + list(V)
        U = np.real(np.fft.fft(V))
        b = list(ii)
        b.append(0)
        b = b + list(np.arange(1 - N, 0))
        w_hat = 1j * np.array(b)
        w_hat = w_hat * U
        W = np.real(np.fft.ifft(w_hat))
        w = np.zeros(N + 1)
        w[1:N] = -W[1:N] / np.sqrt(1 - x[1:N] ** 2)
        w[0] = sum(ii ** 2 * U[ii]) / N + 0.5 * N * U[N]
        w[N] = sum((-1) ** (ii + 1) * ii ** 2 * U[ii]) / N + \
               0.5 * (-1) ** (N + 1) * N * U[N]
        return w

    @staticmethod
    def D1(N):
        h = 2.0 * np.pi / N
        col = np.zeros(N, dtype=np.float)
        col[1:] = 0.5 * (-1.0) ** np.arange(1, N) / np.tan(np.arange(1, N) * h / 2.0)
        row = np.zeros(N)
        row[0] = col[0]
        row[1:] = col[N - 1:0:-1]
        D1 = toeplitz(col, row)
        return D1

    @staticmethod
    def D2(N):
        h = 2.0 * np.pi / N
        col = np.zeros(N, dtype=np.float)
        col[0] = -np.pi ** 2 / (3.0 * h ** 2) - 1.0 / 6.0
        col[1:] = -0.5 * (-1.0) ** np.arange(1, N) / np.sin(0.5 * h * np.arange(1, N)) ** 2
        D2 = toeplitz(col)
        return D2

    @staticmethod
    def fourdif(nfou, mder):
        """
        Fourier spectral differentiation.

        Spectral differentiation matrix on a grid with nfou equispaced points in [0,2pi)

        INPUT
        -----
        nfou: Size of differentiation matrix.
        mder: Derivative required (non-negative integer)

        OUTPUT
        -------
        xxt: Equispaced points 0, 2pi/nfou, 4pi/nfou, ... , (nfou-1)2pi/nfou
        ddm: mder'th order differentiation matrix

        Explicit formulas are used to compute the matrices for m=1 and 2.
        A discrete Fouier approach is employed for m>2. The program
        computes the first column and first row and then uses the
        toeplitz command to create the matrix.

        For mder=1 and 2 the code implements a "flipping trick" to
        improve accuracy suggested by W. Don and A. Solomonoff in
        SIAM J. Sci. Comp. Vol. 6, pp. 1253--1268 (1994).
        The flipping trick is necesary since sin t can be computed to high
        relative precision when t is small whereas sin (pi-t) cannot.

        S.C. Reddy, J.A.C. Weideman 1998.  Corrected for MATLAB R13
        by JACW, April 2003.
        """
        # grid points
        xxt = 2 * np.pi * np.arange(nfou) / nfou
        # grid spacing
        dhh = 2 * np.pi / nfou

        nn1 = np.int(np.floor((nfou - 1) / 2.))
        nn2 = np.int(np.ceil((nfou - 1) / 2.))
        if mder == 0:
            # compute first column of zeroth derivative matrix, which is identity
            col1 = np.zeros(nfou)
            col1[0] = 1
            row1 = np.copy(col1)

        elif mder == 1:
            # compute first column of 1st derivative matrix
            col1 = 0.5 * np.array([(-1) ** k for k in range(1, nfou)], float)
            if nfou % 2 == 0:
                topc = 1 / np.tan(np.arange(1, nn2 + 1) * dhh / 2)
                col1 = col1 * np.hstack((topc, -np.flipud(topc[0:nn1])))
                col1 = np.hstack((0, col1))
            else:
                topc = 1 / np.sin(np.arange(1, nn2 + 1) * dhh / 2)
                col1 = np.hstack((0, col1 * np.hstack((topc, np.flipud(topc[0:nn1])))))
            # first row
            row1 = -col1

        elif mder == 2:
            # compute first column of 1st derivative matrix
            col1 = -0.5 * np.array([(-1) ** k for k in range(1, nfou)], float)
            if nfou % 2 == 0:
                topc = 1 / np.sin(np.arange(1, nn2 + 1) * dhh / 2) ** 2.
                col1 = col1 * np.hstack((topc, np.flipud(topc[0:nn1])))
                col1 = np.hstack((-np.pi ** 2 / 3 / dhh ** 2 - 1 / 6, col1))
            else:
                topc = 1 / np.tan(np.arange(1, nn2 + 1) * dhh / 2) / np.sin(np.arange(1, nn2 + 1) * dhh / 2)
                col1 = col1 * np.hstack((topc, -np.flipud(topc[0:nn1])))
                col1 = np.hstack(([-np.pi ** 2 / 3 / dhh ** 2 + 1 / 12], col1))
            # first row
            row1 = col1

        else:
            # employ FFT to compute 1st column of matrix for mder > 2
            nfo1 = np.int(np.floor((nfou - 1) / 2.))
            nfo2 = -nfou / 2 * (mder + 1) % 2 * np.ones((nfou + 1) % 2)
            mwave = 1j * np.concatenate((np.arange(nfo1 + 1), nfo2, np.arange(-nfo1, 0)))
            col1 = np.real(np.fft.ifft(mwave ** mder * np.fft.fft(np.hstack(([1], np.zeros(nfou - 1))))))
            if mder % 2 == 0:
                row1 = col1
            else:
                col1 = np.hstack(([0], col1[1:nfou + 1]))
                row1 = -col1
        ddm = toeplitz(col1, row1)

        return xxt, ddm


import numpy as np

def stats(arr):
    af = arr.flatten()

    box_bot = np.nanpercentile(af, 25.0)
    box_top = np.nanpercentile(af, 75.0)
    box_center = np.nanpercentile(af, 50.0)  # np.median(af)
    flier_low = np.nanpercentile(af, 0.0)  # np.min(af)
    flier_high = np.nanpercentile(af, 100.0)  # np.max(af)

    return flier_low, box_bot, box_center, box_top, flier_high

def describe(arr):
    flier_low, box_bot, box_center, box_top, flier_high = stats(arr)
    desc = "The array is of shape {0}\n" \
           "The median is {1:.2f}\n" \
           "The first and third quartiles are {2:.2f} and {3:.2f}\n" \
           "The minimum is {4:.2f} and the maximum is {5:.2f}.\n".format(arr.shape, box_center, box_bot, box_top, flier_low, flier_high)

    return desc

# def box_plot(arr):
#     flier_low, box_bot, box_center, box_top, flier_high = stats(arr)


import numpy as np
import matplotlib.pyplot as plt
from utils import gradient_descent_linear_regresion

X1=np.array([])
Y=np.array([])
file = open("data\\ex1data1.txt", "r")
for row in file:
  ar = row.split(",")
  X1 = np.append(X1,float(ar[0]))
  Y = np.append(Y,float(ar[1]))

m = X1.size # should be same to Y.size

iterations = 0
alfa = 0.001 # learning rate
max_iterations = 40000

Teta = np.array([100,100])
X0 = np.ones(m)
X= np.vstack((X0,X1)).T

while iterations <  max_iterations:
  res = gradient_descent_linear_regresion(Teta,X,Y,alfa)
  Teta = res["Teta"]
  J = res["J"]
  iterations += 1
  if(iterations%100 == 0):
      print(iterations,J)

print("J : {:0.2f}, teta0 : {:0.2f} , teta1 : {:0.2f}".format(J,Teta[0], Teta[1]))

H = Teta[0] + Teta[1]*X1

# plots
plt.plot(X1, Y,'x',X1,H)
plt.xlabel("Population in city in 10,000")
plt.ylabel("Profit in 10,000$")
plt.title('garient descent fit for linear regression')
plt.show()


import numpy as np

def toExp(rXs, thetas):

    """
    Every complex number can be written in polar form:
        z = a + jb = sqrt(a**2 + b**2)*exp(arctan(b/a))
    """

    return np.abs(rXs)*np.exp(1j*(np.angle(rXs) + thetas))

def toList(rX):

    """
    Turn a complex number into column matrix:
        z = a + jb = [a, b]
    """

    return np.array([np.real(rX), np.imag(rX)])

def toComplexes(results):

    """
    For convenience, turn a list of numbers into complex numbers
    [real(r1), imag(r1), real(r2), imag(r2), ... , real(rn), imag(rn)]
    = r1, r2, ... , rn
    """

    return [results[i]+results[i+1]*1j for i in range(0, len(results), 2)]

"""
Displacement vector problem
Consider a triangle ABC with a + b = c
Every planar problem derives from 4 cases below. In these cases, only 2
unknowns are considered since a complex vector has 2 components: real and
imaginary. We will look further into finding norm and argument of the
vectors based on these 4 cases.
"""

def CPA1(a, b):

    """
    Complex Polar Algebra, method 1, Theory of Machines and Mechanisms
    The unknowns are C, C_angle
    """

    A, A_angle = np.abs(a), np.angle(a)
    B, B_angle = np.abs(b), np.angle(b)
    C = np.sqrt(A**2 + B**2 + 2*A*B*np.cos(B_angle-A_angle))
    C_angle = np.arctan((A*np.sin(A_angle) + B*np.sin(B_angle))/\
                        (A*np.cos(A_angle) + B*np.cos(B_angle)))
    return C, C_angle

def CPA2(A_angle, B, c):

    """
    Complex Polar Algebra, method 2, Theory of Machines and Mechanisms
    The unknowns are A, B_angle
    Using this case leads to 2 different results. Be sure to check out for
    appropriate conditions to come up with the right solution.
    """

    C, C_angle = np.abs(c), np.angle(c)
    arcsin_res = np.array([np.arcsin(C*np.sin(C_angle-A_angle)/B),
                           np.pi-np.arcsin(C*np.sin(C_angle-A_angle)/B)])
    results = []
    for arcsin in arcsin_res:
        B_angle = A_angle + arcsin
        A = C*np.cos(C_angle-A_angle) - B*np.cos(B_angle-A_angle)
        results.append(np.array([A, B_angle]))
    return results[0], results[1]

def CPA3(A_angle, B_angle, c):

    """
    Complex Polar Algebra, method 3, Theory of Machines and Mechanisms
    The unknowns are A, B
    """

    C, C_angle = np.abs(c), np.angle(c)
    B = C*np.sin(C_angle-A_angle)/np.sin(B_angle-A_angle)
    A = C*np.sin(C_angle-B_angle)/np.sin(A_angle-B_angle)
    return A, B

def CPA4(A, B, c):

    """
    Complex Polar Algebra, method 4, Theory of Machines and Mechanisms
    The unknowns are A_angle, B_angle
    Using this case leads to 2 different results. Be sure to check out for
    appropriate conditions to come up with the right solution.
    """

    C, C_angle = np.abs(c), np.angle(c)
    pm = np.array([1, -1])
    B_angle = C_angle + pm*np.arccos((C**2+B**2-A**2)/(2*B*C))
    A_angle = C_angle + pm*np.arccos((C**2+A**2-B**2)/(2*A*C))
    results = []
    if A*np.sin(A_angle[0] - C_angle) == -B*np.sin(B_angle[0] - C_angle):
        results.append([A_angle[0], B_angle[0]])
        results.append([A_angle[1], B_angle[1]])
        return np.array(results[0]), np.array(results[1])
    else:
        results.append([A_angle[0], B_angle[1]])
        results.append([A_angle[1], B_angle[0]])
        return np.array(results[0]), np.array(results[1])

"Velocity Analysis"

def vel(r, omg=0, v_T=0):

    """
    Complex-algebraic velocity analysis, Theory of Machines and Mechanisms,
    returns total velocity (wrt to fixed frame of reference)

    Take first derivative of displacement vector:
        d
        -- (r) = v = R_T*exp(j*arg(r)) + j*omg*r
        dt

    It can also be written in matrix form as:
    [cos(angle(r)), -imag(r)] [ v_T ]
    [sin(angle(r)),  real(r)] [ omg ]

    The first row is real. The second row is imaginary.

    v_T: translational velocity
    omg: angular velocity
    r: displacement vector
    """

    return v_T*np.exp(1j*np.angle(r)) + 1j*omg*r

def vCPA1(v_known, r1, r2, omg2=0, v2_T=0):

    """
    Complex-algebraic velocity analysis, Theory of Machines and Mechanisms
    The unknowns are v1_T, omg1
    v_known + v_r1 = v_r2
    """

    A = [[-np.cos(np.angle(r1)),  np.imag(r1)],
         [-np.sin(np.angle(r1)), -np.real(r1)]]
    b = [np.real(v_known) - (np.cos(np.angle(r2))*v2_T - np.imag(r2)*omg2),
         np.imag(v_known) - (np.sin(np.angle(r2))*v2_T + np.real(r2)*omg2)]
    return np.linalg.solve(A, b)

def vCPA2(v_known, r1, r2, omg1=0, v2_T=0):

    """
    Complex-algebraic velocity analysis, Theory of Machines and Mechanisms
    The unknowns are v1_T, omg2
    v_known + v_r1 = v_r2
    """

    A = [[-np.cos(np.angle(r1)), -np.imag(r2)],
         [-np.sin(np.angle(r1)),  np.real(r2)]]
    b = [np.real(v_known) - ( np.imag(r1)*omg1 + np.cos(np.angle(r2))*v2_T),
         np.imag(v_known) - (-np.real(r1)*omg1 + np.sin(np.angle(r2))*v2_T)]
    return np.linalg.solve(A, b)

def vCPA3(v_known, r1, r2, omg1=0, omg2=0):

    """
    Complex-algebraic velocity analysis, Theory of Machines and Mechanisms
    The unknowns are V1_T, V2_T
    v_known + v_r1 = v_r2
    """

    A = [[-np.cos(np.angle(r1)), np.cos(np.angle(r2))],
         [-np.sin(np.angle(r1)), np.sin(np.angle(r2))]]
    b = [np.real(v_known) - ( np.imag(r1)*omg1 - np.imag(r2)*omg2),
         np.imag(v_known) - (-np.real(r1)*omg1 + np.real(r2)*omg2)]
    return np.linalg.solve(A, b)

def vCPA4(v_known, r1, r2, v1_T=0, v2_T=0):

    """
    Complex-algebraic velocity analysis, Theory of Machines and Mechanisms,
    The unknowns are omg1, omg2
    v_known + v_r1 = v_r2
    """

    A = [[ np.imag(r1), -np.imag(r2)],
         [-np.real(r1),  np.real(r2)]]
    b = [np.real(v_known) - (-np.cos(np.angle(r1))*v1_T + np.cos(np.angle(r2))*v2_T),
         np.imag(v_known) - ( np.sin(np.angle(r1))*v1_T + np.sin(np.angle(r2))*v2_T)]
    return np.linalg.solve(A, b)

"Acceleration Analysis"

"v_T: translational velocity"
"a_T: translational acceleration"

def coriolis(theta, omg=0, v_T=0): return 2j*v_T*omg*np.exp(1j*theta)

def centripetal(rX, omg=0): return -rX*omg**2

def acc(rX, omg=0, alp=0, v_T=0, a_T=0):

    """
    Complex-algebraic velocity analysis, Theory of Machines and Mechanisms,
    returns total acceleration (wrt to fixed frame of reference)

    Convetionally, there are 4 elements in an acceleration vector:
        - Centripetal acceleration: -omg**2*r
        - Tangential acceleration: alp*r
        - Coriolis acceleration: norm = |2*v_T*omg|
        - Relative acceleration: depends on many things

    Take first derivative of velocity vector:
        d         d
        -- (v) =  -- (R_T*exp(j*arg(r)) + j*omg*r)
        dt        dt

    Every acceleration vector on a plane can be written as
    [cos(angle(r)), -imag(r)] [ a_T ] _ [real(r)*omg**2 + 2*omg*v_T*sin(angle(r))]
    [sin(angle(r)),  real(r)] [ alp ]   [imag(r)*omg**2 - 2*omg*v_T*cos(angle(r))]

    The first row is real. The second row is imaginary.

    r: displacement vector
    omg: angular velocity
    v_T: translational velocity
    alp: angular acceleration
    a_T: translational acceleration
    """

    return a_T*np.exp(1j*np.angle(rX)) + 1j*alp*rX +\
        coriolis(np.angle(rX), omg, v_T) + centripetal(rX, omg)

def aCPA1(a_known, r1, r2, omg1=0, omg2=0, alp2=0, v1_T=0, v2_T=0, a2_T=0):

    """
    a_known + a_1 = a_2
    return a1_T, alp1
    """

    a1_known = centripetal(r1, omg1) + coriolis(np.angle(r1), omg1, v1_T)
    a2_known = centripetal(r2, omg2) + coriolis(np.angle(r2), omg2, v2_T)
    free_coeff = a_known + a1_known - a2_known

    A = [[-np.cos(np.angle(r1)),  np.imag(r1)],
         [-np.sin(np.angle(r1)), -np.real(r1)]]
    b = [np.real(free_coeff) - (np.cos(np.angle(r2))*a2_T - np.imag(r2)*alp2),
         np.imag(free_coeff) - (np.sin(np.angle(r2))*a2_T + np.real(r2)*alp2)]
    return np.linalg.solve(A, b)

def aCPA2(a_known, r1, r2, omg1=0, omg2=0, alp1=0, v1_T=0, v2_T=0, a2_T=0):

    """
    a_known + a_1 = a_2
    return a1_T, alp2
    """

    a1_known = centripetal(r1, omg1) + coriolis(np.angle(r1), omg1, v1_T)
    a2_known = centripetal(r2, omg2) + coriolis(np.angle(r2), omg2, v2_T)
    free_coeff = a_known + a1_known - a2_known

    A = [[-np.cos(np.angle(r1)), -np.imag(r2)],
         [-np.sin(np.angle(r1)),  np.real(r2)]]
    b = [np.real(free_coeff) - ( np.imag(r1)*alp1 + np.cos(np.angle(r2))*a2_T),
         np.imag(free_coeff) - (-np.real(r1)*alp1 + np.sin(np.angle(r2))*a2_T)]
    return np.linalg.solve(A, b)

def aCPA3(a_known, r1, r2, omg1=0, omg2=0, alp1=0, alp2=0, v1_T=0, v2_T=0):

    """
    a_known + a_1 = a_2
    return a1_T, a2_T
    """

    a1_known = centripetal(r1, omg1) + coriolis(np.angle(r1), omg1, v1_T)
    a2_known = centripetal(r2, omg2) + coriolis(np.angle(r2), omg2, v2_T)
    free_coeff = a_known + a1_known - a2_known

    A = [[-np.cos(np.angle(r1)), np.cos(np.angle(r2))],
         [-np.sin(np.angle(r1)), np.sin(np.angle(r2))]]
    b = [np.real(free_coeff) - ( np.imag(r1)*alp1 - np.imag(r2)*alp2),
         np.imag(free_coeff) - (-np.real(r1)*alp1 + np.real(r2)*alp2)]
    return np.linalg.solve(A, b)

def aCPA4(a_known, r1, r2, omg1=0, omg2=0, v1_T=0, v2_T=0, a1_T=0, a2_T=0):

    """
    a_known + a_1 = a_2
    return alp1, alp2
    """

    a1_known = centripetal(r1, omg1) + coriolis(np.angle(r1), omg1, v1_T)
    a2_known = centripetal(r2, omg2) + coriolis(np.angle(r2), omg2, v2_T)
    free_coeff = a_known + a1_known - a2_known

    A = [np.imag([r1, -r2]),
         np.real([-r1, r2])]
    b = [np.real(free_coeff) - (-np.cos(np.angle(r1))*a1_T + np.cos(np.angle(r2))*a2_T),
         np.imag(free_coeff) - (-np.sin(np.angle(r1))*a1_T + np.sin(np.angle(r2))*a2_T)]
    return np.linalg.solve(A, b)


import jax.numpy as np
import jax.scipy.stats as stats
import jax.scipy.linalg as linalg
import jax
import util
import matplotlib.pyplot as plt
jax.config.update("jax_enable_x64", True)

@jax.jit
def log_prior(theta, sigma0):
    return np.sum(stats.norm.logpdf(theta, scale=sigma0))

@jax.jit
def log_likelihood_per_sample(theta, x, cov):
    return stats.multivariate_normal.logpdf(theta, mean=x, cov=cov)

@jax.jit
def log_likelihood_per_sample_fast(theta, x, chol):
    dim = theta.size
    # const = -0.5 * dim * np.log(2 * np.pi)
    # logdet = -np.sum(np.log(chol.diagonal()))
    z = linalg.solve_triangular(chol, x - theta, lower=True)
    return -0.5 * np.sum(z**2) #+ const + logdet

class GaussModel:
    def __init__(self, dim):
        self.dim = dim
        self.sigma0 = 100
        self.tau0 = 1 / self.sigma0**2
        self.cov = np.eye(dim)
        for i in range(int(self.dim / 2)):
            self.cov = jax.ops.index_update(
                self.cov, jax.ops.index[2*i:2*(i+1), 2*i:2*(i+1)],
                np.array(((1, 0.999), (0.999, 1))) / (i+1)
            )

        self.L = np.linalg.cholesky(self.cov)
        self.true_mean = np.hstack((np.array((0, 3)), np.zeros(dim - 2)))

    def log_prior(self, theta):
        return log_prior(theta, self.sigma0)

    def log_likelihood_per_sample(self, theta, x):
        return log_likelihood_per_sample_fast(theta, x, self.L)

    def generate_data(self, n):
        key = jax.random.PRNGKey(46283648)
        return jax.random.multivariate_normal(key, self.true_mean, self.cov, (n,))

    def compute_posterior_params(self, data):
        n, d = data.shape
        # See Bayesian Data Analysis, section 3.5
        # or https://stats.stackexchange.com/q/28744
        sigma0_mat = np.eye(self.dim) * self.sigma0
        term1 = sigma0_mat @ np.linalg.inv(sigma0_mat + self.cov / n)
        mu_post = term1 @ np.mean(data, axis=0).reshape((-1, 1))
        cov_post = term1 @ self.cov / n
        return (mu_post.reshape(-1,), cov_post)

    def generate_true_posterior(self, samples, data, temp_scale=1, key=None):

        if temp_scale != 1:
            raise Exception("GaussModel does not support sampling tempered posteriors")
        if key is None:
            key = jax.random.PRNGKey(434927)
        mu_post, cov_post = self.compute_posterior_params(data)

        return jax.random.multivariate_normal(key, mu_post, cov_post, (samples,))

    def plot_posterior(self, ax, data):
        mu_post, cov_post = self.compute_posterior_params(data)
        xs = np.linspace(-0.01, 0.01, 1000) + mu_post[0]
        ys = np.linspace(-0.01, 0.01, 1000) + mu_post[1]
        X, Y = np.meshgrid(xs, ys)
        Z = jax.vmap(jax.vmap(lambda x: stats.multivariate_normal.pdf(x, mean=mu_post, cov=cov_post),
            in_axes=1
        ), in_axes=2)(np.stack((X, Y)))
        ax.contour(X, Y, Z)

def get_problem(dim, n):
    model = GaussModel(dim)
    data = model.generate_data(n)
    problem = util.Problem(
        model.log_likelihood_per_sample, model.log_prior,
        data, 1, model.true_mean, model.generate_true_posterior,
        lambda problem, ax: model.plot_posterior(ax, problem.data)
    )
    return problem

if __name__ == "__main__":
    dim = 2
    model = GaussModel(dim)
    data = model.generate_data(100000)
    posterior = model.generate_true_posterior(1000, data)
    fig, ax = plt.subplots()
    model.plot_posterior(ax, data)
    plt.scatter(posterior[:, 0], posterior[:, 1])
    plt.show()
    for i in range(dim):
        print(np.quantile(posterior[:, i], np.array([0.01, 0.99])))

    # x = np.repeat(1, dim)
    # theta = np.repeat(2, dim)
    # print(log_likelihood_per_sample(x, theta, model.cov))
    # L = np.linalg.cholesky(model.cov)
    # print(log_likelihood_per_sample_fast(x, theta, L))

    # log_likelihood_per_sample(np.zeros(dim), np.zeros(dim), np.eye(dim))
    # log_likelihood_per_sample_fast(np.zeros(dim), np.zeros(dim), np.eye(dim))
    # from timeit import timeit
    # print(timeit(lambda: log_likelihood_per_sample(x, theta, model.cov), number=1))
    # print(timeit(lambda: log_likelihood_per_sample_fast(x, theta, L), number=1))


import numpy as np
import matplotlib.pyplot as plt

F = np.array([[0, 0], [0, 1]])
E = np.array([-8, 0])

# Eqn of Parabola is AtFA + EA = 0
# Eqn of tangent must be AtFA1 + 0.5E(A+A1) = 0 (SS1=0)[Given, it passes through [-8, 0]]
	# => GA = 0

Threshold = 0.02 #Define a small threshold to get an approximate solution

l = np.linspace(-10, 10, 10000)
sol1 = 0

for x in l:
	D = np.array([8, x])
	K = np.matmul(D, F)
	L = np.matmul(K, D)
	M = np.matmul(E, D)
	N= np.abs(L + M)
	if N < Threshold:
		sol1 = x

l = np.linspace(10, -10, 10000)
sol2 = 0

for x in l:
	D = np.array([8, x])
	K = np.matmul(D, F)
	L = np.matmul(K, D)
	M = np.matmul(E, D)
	N= np.abs(L + M)
	if N < Threshold:
		sol2 = x

print(sol1)
print(sol2)

A1 = np.array([8, sol1])
A2 = np.array([8, sol2])

Area = 0.5*np.linalg.det([[A1[0], A1[1], 1],[A2[0], A2[1], 1], [2, 0, 1] ])

print(np.abs(Area))

p = np.linspace(0, 15, 10000)
plt.plot(p, np.sqrt(8*p), color = 'blue', label = 'Given Parabola')
plt.plot(p, -np.sqrt(8*p), color = 'blue')

plt.plot([A1[0], -8], [A1[1], 0], color = 'black', label = 'Tangents')
plt.plot([A2[0], -8], [A2[1], 0], color = 'black')

Focus = np.array([2, 0])

plt.plot([A2[0], Focus[0]], [A2[1], Focus[1]], color = 'red')
plt.plot([A1[0], Focus[0]], [A1[1], Focus[1]], color = 'red')
plt.plot([A2[0], A1[0]], [A2[1], A1[1]], color = 'red', label = 'Area Required to Calculate')

plt.text(-8 - 0.5, 0 + 0.5 , "0")
plt.text(A1[0] - 0.5, A1[1] + 0.5 , "A(8, 8)")
plt.text(A2[0] - 0.5, A2[1] + 0.5 , "B(8, -8)")
plt.text(2 - 0.5, 1 + 0.5 , "F(2, 0)")

plt.plot(A1[0], A1[1], "o", color = 'black')
plt.plot(A2[0], A2[1], "o", color = 'black')
plt.plot(-8, 0, "o", color = 'black')
plt.plot(2, 0, "o", color = 'black')

plt.legend()
plt.grid()

plt.savefig('/home/gautham/Desktop/AIML/Fig.png')

plt.show()


from   math import sin, cos, e
import matplotlib.pyplot as plt
import numpy as np

def f(x):
    return x**2-2
def fp(x):
    return 2*x

def iterationMethod(a, b, f, fp, eps_x, eps_y, max_n):
    #третий аргумент return - false , если корень не найден
    n = 0
    for x0 in [a,b]:

        #подбираем lambd
        #lmbd == false => lambda = 1
        #lmbd == true  => lambda = 1/fp(x)
        lmbd = False
        x1 = x0 - f(x0)
        x2 = x1 - f(x1)

        if abs(x2 - x1) > abs(x1 - x0):
            lmbd = True
            if (fp(x1) == 0) or (fp(x0) == 0)  :
                return x1, n, False
            x1 = x0 - f(x0)/fp(x0)
            x2 = x1 - f(x1)/fp(x1)
            if abs(x2 - x1) > abs(x1 - x0):
                return x1, n, False

        while True:
            n += 1

            if not lmbd:
                x1 = x0 - f(x0)
            else:
                x1 = x0 - f(x0)/fp(x0)
            #условия завершения
            if eps_x > 0:
                if abs(x1 - x0) < eps_x:
                    return x1, n, True
            if eps_y > 0:
                if abs(f(x1) - f(x0)) < eps_y:
                    return x1, n, True
            if n >= max_n:
                return x1, n, True
            if  not (a<=x1<=b):
                #если произошел выход за границы
                #обычно срабатывает при отстутсвии корня
                break

            x0 = x1

        #если не найдено корней, то
        return x1, n, False

def generateRootsData(a, b, h, eps_x, eps_y, max_n):
    ah = a
    bh = a + h
    roots_data = []
    step = 1
    while bh <= b:
        x, n, finded = iterationMethod(ah, bh, f, fp, eps_x, eps_y, max_n)
        if finded:
            left_border  = ah
            right_border = bh
            root         = x
            value        = f(x)
            iteration_n  = n
            if n >= max_n:
                err_code = 1
            else:
                err_code = 0
            if not x in [i[2] for i in roots_data]:
                roots_data.append([left_border, right_border, root, value, iteration_n, err_code])

        step += 1
        ah = a + h * (step - 1)
        bh = a + h * step
        if bh > b:
            if ah > b:
                break
            else:
                bh = b
    return roots_data

def inputData():
    inp   = input('Введите интервал поиска [A,B] в формате "A,B":  ')
    a,b   = map(float, inp.split(','))
    inp   = input('Введите шаг h:                                  ')
    h     = float(inp)
    inp   = input('Введите ? по x, если требуется:                 ')
    if inp.strip() == '':
        eps_x = 0
    else:
        eps_x = float(inp)
    inp   = input('Введите ? по y, если требуется:                 ')
    if inp.strip() == '':
        eps_y = 0
    else:
        eps_y = float(inp)
    inp   = input('Введите предельное число итераций n_max >= 100: ')
    max_n = int(inp)
    if not (max_n>=100):
        print('Неверный ввод!')
        return 0
    return a, b, h, eps_x, eps_y, max_n

def printTable(data, max_n):
    # расчет форматированной строки
    maxalen     = len( str(data[-1][0]) + str(data[-1][1]) )
    abrowlen    = maxalen + 5
    iterrowlen  = len('кол-во итераций') + 2
    errrowlen   =  len('код ошибки') + 2
    table_width = (1 + 9 + 1 + abrowlen + 1
                  + 14 + 1 + 15 + 1 + iterrowlen + 1 + errrowlen+1)
    t_row  = ('|{:^9}|{:^'+str(abrowlen)+'}|{:^14}|{:^15}|{:^'+str(iterrowlen)
              +'}|{:^'+str(errrowlen)+'}|')
    t_head = t_row.format('№ корня','[А,B]','корень','f(x)','кол-во итераций',
                          'код ошибки')

    print('-'*table_width)
    print(t_head)
    print('|','-'*(table_width-2),'|', sep='')
    n = 0
    for i in data:
        n += 1
        arg1 = n
        arg2 = ('['+'{:g}'.format(i[0])+','
                +'{:g}'.format(i[1])+']')
        arg3 = '{:g}'.format(i[2])
        arg4 = '{:.0e}'.format(i[3])
        arg5 = i[4]
        arg6 = i[5]
        if arg6 != 0:
            arg3 = arg4 = '-'
        print(t_row.format(arg1, arg2, arg3, arg4, arg5, arg6 ))
    print('-'*table_width)

    print('Коды ошибок: ')
    print('0 - нет ошибки')
    print('1 - достигнуто максимальное количество итераций')

def drawPlot(data, a, b):
    x_f = np.linspace(a,b, 1000)
    y_f = list(map(f, x_f))
    x_roots = []
    y_roots = []
    point_text = []
    for row in range(len(data)):
        if data[row][5] == 0:
            x_roots.append(data[row][2])
            y_roots.append(data[row][3])
            point_text.append(row+1)

    bottom = min(y_f)
    top    = max(y_f)
    if bottom > 0:
        bottom*=0.9
    elif bottom < 0:
        bottom*=1.1
    #else:
    #    bottom = -top * 0.1
    if top > 0:
        top*=1.1
    elif top < 0:
        top*=0.9
    #else:
    #    top = -bottom * 0.1

    ax = plt.subplot()

    plt.axis([a,b, bottom, top])
    plt.title('Finding roots of f(x) with fixed-point iteration method')
    plt.xlabel('x')
    plt.ylabel('f(x)')

    plt.plot(x_f,y_f, '-', color='blue', label='f(x)')
    plt.plot(x_roots, y_roots, 'bo', color='red', label = 'finded roots')

    for i in range(len(x_roots)):
        ax.annotate('#'+str(point_text[i]), (x_roots[i],y_roots[i]))
    plt.legend()

    plt.plot([a,b], [0,0], '-', color='black')
    plt.plot([0,0], [bottom,top], '-', color='black')

    plt.show()

def main():
    while 1:
        print('=== Уточнение корня методом подбора с итерацией ===')
        #ввод данных
        inp_data = inputData()
        if inp_data == 0:
            return
        else:
            a, b, h, eps_x, eps_y, max_n = inp_data

        #генерация и вывод таблицы
        print()
        print('               -  поиск корней  -                  ')
        roots_data = generateRootsData(a, b, h, eps_x, eps_y, max_n)
        if len(roots_data) == 0:
            print('Корней не найдено!')
        else:
            printTable(roots_data, max_n)
        drawPlot(roots_data, a, b)

        #просьба еще одного ввода
        print()
        inp = input('Введите 1, чтобы ввести новые данные: ')
        if inp!='1':
            break
        else:
            print()
            print()

main()

Numerical algorithms/Root finding/Fixed-point iteration method.py

Numerical analysis/Fixed-point iteration method.py


import numpy as np

class Bisection(object):
    def __init__(self, f=None, **extras):
        """
        f : a function f(x) that returns 'y', the function applied to x
        """
        self.f = f

    def solve(self, init_a=None, init_b=None, tol=None, max_it=50, **extras):
        """fast implementation of the bisection method
        init_a : bottom bound of the initial interval
        init_b : top bound of the initial interval
        tol : maximum absolute error after which the method should stop. (e.g. 1e-15)
        max_it : maximum number of iterations after which the method should stop
        """
        i = 0
        c = 0
        a = init_a
        b = init_b
        while i < max_it:
            c = ( (a + b) / 2 )
            if abs(b-c) < tol:
                break
            elif c == 0:
                break
            if np.sign( self.f(b) ) * np.sign( self.f(c) ) <= 0:
                a = c
            else:
                b = c
            i+=1

        return c

    def vsolve(self, init_a=None, init_b=None, tol=None, max_it=50, **extras):
        """verbose implementation of the bisection method
        init_a : bottom bound of the initial interval
        init_b : top bound of the initial interval
        tol : maximum absolute error after which the method should stop. (e.g. 1e-15)
        max_it : maximum number of iterations after which the method should stop
        """

        i = 0
        c = 0
        a = init_a
        b = init_b

        iteration_list = []
        dist_list = []
        estimate_list = []

        while i < max_it:
            co = c
            c = self._half(a,b)
            dist = abs(c-co)

            iteration_list.append(i)
            dist_list.append(dist)
            estimate_list.append(c)

            if abs(b-c) < tol:
                break

            if np.sign( self.f(b) ) * np.sign( self.f(c) ) <= 0:
                a = c
            else:
                b = c

            i+=1

        return iteration_list, estimate_list, dist_list

    def _half(self,a,b):
        return (a + b) / 2

    def __name__(self):
        return "Bisection"

    # def __repr__(self):
        # return "Bisection "
        # raise NotImplementedError

    # def __str__(self):
    #     return "Bisection method"


import numpy as np
a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(a>3) #Where is A>3?

'''
[[False False False]
 [ True  True  True]
 [ True  True  True]]

gives the above.
So in 0th list, none are true. Then you have 1st list, 0th is true. 1st list 1th is true. 1st list 2nd is true
so you have 1-0, 1-1, 1-2, where 1st # is the list # and 2nd # is the index in that list
In the 2nd list (3rd one), 0th is true, 1st etc.

'''
print(np.nonzero(a>3))
'''
(array([1, 1, 1, 2, 2, 2]), array([0, 1, 2, 0, 1, 2]))
gives above. So the first array is the list #, and 2nd array is the index within that list
so list 1  number 0, list 1 number 1, list 1 number 2
list 2 number 0, list 2 number 1, list 2 number 2

'''


import numpy as np

def Maindiagonal(n, r):
    """

    :param n: 矩阵的维度
    :param r: 对角线元素
    :return: 一个对角矩阵
    """
    a = np.zeros((n, n))
    for i in range(n):
        a[i][i] = r
    return a

# r1:main r2:上负对角线元素　r3:下负对角线元素
def tri_diagonal(n, r1, r2, r3):
    """

    :param n: 矩阵的维度
    :param r1: 主对角元素
    :param r2: 次上对角元素
    :param r3: 次下对角元素
    :return: 对称正定的矩阵(或者说是三对角矩阵)
    """
    a = np.zeros((n, n))
    for i in range(n):
        if i == 0:
            a[i][i] = r1
            a[i][i + 1] = r2
        elif i == n - 1:
            a[i][i] = r1
            a[i][i - 1] = r3
        else:
            a[i][i] = r1
            a[i][i + 1] = r2
            a[i][i - 1] = r3
    return a

def M_tri_diagonal(nx, ny, b1, b2, b3):
    """

    :param nx:
    :param ny:
    :param b1:正对角块矩阵
    :param b2: 次上对角块矩阵
    :param b3: 次下对角块矩阵
    :return: 一个五带的对称正定矩阵
    """
    N = nx * ny
    a = np.zeros((N, N))
    for i in range(nx):
        if i == 0:
            a[0:ny, 0:ny] = b1
            a[0:ny, ny:2 * ny] = b2
        elif i == nx - 1:
            a[N - ny:N, N - ny:N] = b1
            a[N - ny:N, N - 2 * ny:N - ny] = b3
        else:
            a[i * ny:(i + 1) * ny, i * ny:(i + 1) * ny] = b1
            a[i * ny:(i + 1) * ny, (i + 1) * ny:(i + 2) * ny] = b2
            a[i * ny:(i + 1) * ny, (i - 1) * ny:i * ny] = b3
    return a

def five_diagonal(nx, ny, r1, r2, r3, r4, r5):
    """
    返回一个具有五带非零元素的对称正定矩阵
    :param nx: x轴的节点数（看情况可以是内节点和外界点数）
    :param ny: y轴的节点数
    :param r1: 主对角
    :param r2: 上次对角
    :param r3: 下次对角
    :param r4: 上次角的右边非零元素
    :param r5: 下次对角左边非零元素
    :return: 一个五带的对称正定矩阵
    """
    d = tri_diagonal(ny, r1, r2, r3)  # 主对角块矩阵
    b = Maindiagonal(ny, r4)  # 上对角块矩阵
    c = Maindiagonal(ny, r5)  # 下对角块矩阵
    a = M_tri_diagonal(nx, ny, d, b, c)
    return a

def csrmatrix(A, data, indices, indptr):
    k = 0
    nr = A.shape[0]
    nc = A.shape[1]
    for i in range(nr):
        for j in range(nc):
            if A[i][j] != 0:
                data[k] = A[i][j]
                indices.append(j)
                k += 1
        indptr.append(k)

# 写一些测试内容，在次判断下导入该包的文件不会输出这些内容
# 也可以单独写一个函数来写测试内容，然后在这里调用它,比如def main():
if __name__ == "__main__":
    print("test")
    # main()


from math import e, sin, cos
import pandas as pd
from sympy import Symbol, integrate, lambdify

x = Symbol('x')

print("Численные методы решения задачи Коши для обыкновенного дифференциального уравнения\nВариант 5\n")
print('y´(x)= - y(x) + sin(x), y(0)=1')
dy = lambda x, y: - y + sin(x)
y = lambda x: 1 / 2 * (3 * e ** (-x) - cos(x) + sin(x))

def series(x_):
    return 1 - (x_ - x0) + (x_ - x0) ** 2 - 1 / 3 * (x_ - x0) ** 3 + 1 / 24 * (x_ - x0) ** 4 - 1 / 120 * (x_ - x0) ** 5

def def_int(func, a, b):
    int_ = lambdify(x, integrate(func, x))
    return int_(b) - int_(a)

def kf(f_, arr, A, B):
    x_ = list(arr['x'])
    y_ = list(arr['T_n(x)'])
    w = (x - x_[0]) * (x - x_[1]) * (x - x_[2]) * (x - x_[3]) * (x - x_[4])
    d_w = lambda x: (5 * x ** 4 -
                     4 * x ** 3 * (sum(x_)) +
                     3 * x ** 2 * (x_[0] * x_[1] + x_[0] * x_[2] + x_[0] * x_[3] + x_[0] * x_[4] + x_[1] * x_[2] +
                                   x_[1] * x_[3] + x_[1] * x_[4] + x_[2] * x_[3] + x_[2] * x_[4] + x_[3] * x_[4]) -
                     2 * x ** 1 * (x_[0] * x_[1] * x_[2] + x_[0] * x_[1] * x_[3] + x_[0] * x_[1] * x_[4] +
                                   x_[0] * x_[2] * x_[3] + x_[0] * x_[2] * x_[4] + x_[0] * x_[3] * x_[4] +
                                   x_[1] * x_[2] * x_[3] + x_[1] * x_[2] * x_[4] + x_[1] * x_[3] * x_[4] +
                                   x_[2] * x_[3] * x_[4]) +
                     1 * x ** 0 * (
                             x_[0] * x_[1] * x_[2] * x_[3] + x_[0] * x_[1] * x_[2] * x_[4] + x_[0] * x_[1] * x_[4] *
                             x_[3] +
                             x_[0] * x_[4] * x_[2] * x_[3] + x_[4] * x_[1] * x_[2] * x_[3])
                     )

    res = sum(def_int(w / (x - x_[i]) / d_w(x_[i]), A, B) * f_(x_[i], y_[i]) for i in range(5))
    return y_[4] + res

def taylor():
    global df
    df = pd.DataFrame(
        [(x0 + k * h, series(x0 + k * h), abs(series(x0 + k * h) - y(x0 + k * h))) for k in
         range(-2, n + 1)]).rename(columns={0: 'x', 1: 'T_n(x)', 2: '|y - T_n(x)|'}, axis=None)
    return

def adams():
    global df
    for k in range(3, n + 1):
        xi = float(df.tail(1)['x'])
        d = kf(dy, df[-5:], xi, xi + h)
        df = df.append({'x': xi + h, 'T_n(x)': d}, ignore_index=True)

    return

def runge_kutta(xi, yi):
    global df
    for k in range(n):
        k1 = h * dy(xi, yi)
        k2 = h * dy(xi + h / 2, yi + k1 / 2)
        k3 = h * dy(xi + h / 2, yi + k2 / 2)
        k4 = h * dy(xi + h, yi + k3)
        xi += h
        yi += 1 / 6 * (k1 + 2 * k2 + 2 * k3 + k4)
        df = df.append({'x': xi, 'y': yi}, ignore_index=True)
        # print(xi, yi)
    return

def euler(xi, yi):
    global df
    for k in range(n):
        yi += h * dy(xi, yi)
        xi += h
        df = df.append({'x': xi, 'y': yi}, ignore_index=True)
        # print(xi, yi)
    return

def euler_mod(xi, yi):
    global df
    for k in range(n):
        yi_2 = yi + h / 2 * dy(xi, yi)
        yi += h * dy(xi + h / 2, yi_2)
        xi += h
        df = df.append({'x': xi, 'y': yi}, ignore_index=True)
    return

def euler_cauchy(xi, yi):
    global df
    for k in range(n):
        t = yi + h * dy(xi, yi)
        yi += h / 2 * (dy(xi, yi) + dy(xi + h, t))
        xi += h
        df = df.append({'x': xi, 'y': yi}, ignore_index=True)
    return

print('h = 0.1\nN = 10')
h = 0.1
n = 10
x0 = 0
y0 = 1

# Точное решение
solution = pd.DataFrame([(x0 + k * h - 2, y(x0 + k * h)) for k in range(-2, n + 1)])
solution = solution.rename(columns={0: 'x', 1: 'y'})
solution.index -= 2
print("\nТаблица значений точного решения")
print(solution)

# Ряд Тейлора
df = pd.DataFrame(columns=['x', 'y'])
taylor()
df.index -= 2
print('\nМетод разложения в р. Тейлора:')
print(df[:5])

# Метод Адамса
df = df[:5]
df = df.drop('|y - T_n(x)|', axis=1)
adams()
df = df.rename(columns={'T_n(x)': 'y'})
df.index -= 2
print('\nЭкстраполяционный метод Адамса:')
print(df[5:])
print('Абс. факт. погрешность метода Адамса для y(x_n):', abs(list(df['y'])[-1] - list(solution['y'])[-1]))
df = df.iloc[0:0]

# Метод Рунге-Кутты
runge_kutta(0, 1)
print('\nМетод Рунге-Кутты:')
df.index += 1
print(df[:n])
print('Абс. факт. погрешность метода Рунге-Кутты для y(x_n):', abs(list(df['y'])[-1] - list(solution['y'])[-1]))
df = df.iloc[0:0]

# Метод Эйлера
euler(0, 1)
print('\nМетод Эйлера:')
df.index += 1
print(df)
print('Абс. факт. погрешность метода Эйлера для y(x_n):', abs(list(df['y'])[-1] - list(solution['y'])[-1]))
df = df.iloc[0:0]

# Модифицированный метод Эйлера
euler_mod(0, 1)
print('\nМодифицированный метод Эйлера:')
df.index += 1
print(df)
print('Абс. факт. погрешность мод. метода Эйлера для y(x_n):', abs(list(df['y'])[-1] - list(solution['y'])[-1]))
df = df.iloc[0:0]

# Метод Эйлера-Коши
euler_cauchy(0, 1)
print('\nМетод Эйлера-Коши:')
df.index += 1
print(df)
print('Абс. факт. погрешность метода Эйлера-Коши для y(x_n):', abs(list(df['y'])[-1] - list(solution['y'])[-1]))
df = df.iloc[0:0]


import numpy as np
import matplotlib.pyplot as plt

N = 50                                # Number of points in each direction
x_start, x_end = -2.0, 2.0            # x-direction boundaries
y_start, y_end = -1.0, 1.0            # y-direction boundaries
x = np.linspace(x_start, x_end, N)    # computes a 1D-array for x
y = np.linspace(y_start, y_end, N)    # computes a 1D-array for y
X, Y = np.meshgrid(x, y)

kappa = 1.0                           # strength of the doublet
x_doublet, y_doublet = 0.0, 0.0       # location of the doublet

u_inf = 1.0

def get_velocity_doublet(strength, xd, yd, X, Y):

    u = - strength / (2 * np.pi) * ((X - xd)**2 - (Y - yd)
                                    ** 2) / ((X - xd)**2 + (Y - yd)**2)**2
    v = - strength / (2 * np.pi) * 2 * (X - xd) * \
        (Y - yd) / ((X - xd)**2 + (Y - yd)**2)**2

    return u, v

def get_stream_function_doublet(strength, xd, yd, X, Y):

    psi = - strength / (2 * np.pi) * (Y - yd) / ((X - xd)**2 + (Y - yd)**2)

    return psi

# computes the velocity field on the mesh grid
u_doublet, v_doublet = get_velocity_doublet(kappa, x_doublet, y_doublet, X, Y)

# computes the stream-function on the mesh grid
psi_doublet = get_stream_function_doublet(kappa, x_doublet, y_doublet, X, Y)

# freestream velocity components
u_freestream = u_inf * np.ones((N, N), dtype=float)
v_freestream = np.zeros((N, N), dtype=float)

# stream-function of the freestream flow
psi_freestream = u_inf * Y

# superposition of the doublet on the freestream flow
u = u_freestream + u_doublet
v = v_freestream + v_doublet
psi = psi_freestream + psi_doublet

# plots the streamlines
size = 10
plt.figure(figsize=(size, (y_end - y_start) / (x_end - x_start) * size))
plt.xlabel('x', fontsize=16)
plt.ylabel('y', fontsize=16)
plt.xlim(x_start, x_end)
plt.ylim(y_start, y_end)
plt.streamplot(X, Y, u, v, density=2, linewidth=1,
               arrowsize=1, arrowstyle='->')
plt.scatter(x_doublet, y_doublet, color='#CD2305', s=80, marker='o')

# calculates the cylinder radius and adds it to the figure
R = np.sqrt(kappa / (2 * np.pi * u_inf))
circle = plt.Circle((0, 0), radius=R, color='#CD2305', alpha=0.5)
plt.gca().add_patch(circle)

# calculates the stagnation points and adds it to the figure
x_stagn1, y_stagn1 = +np.sqrt(kappa / (2 * np.pi * u_inf)), 0
x_stagn2, y_stagn2 = -np.sqrt(kappa / (2 * np.pi * u_inf)), 0
plt.scatter([x_stagn1, x_stagn2], [y_stagn1, y_stagn2],
            color='g', s=80, marker='o')

plt.show()

gamma = 4 * np.pi * u_inf * R

x_vortex, y_vortex = 0.0, 0.0    # location of the vortex

def get_velocity_vortex(strength, xv, yv, X, Y):

    u = + strength / (2 * np.pi) * (Y - yv) / ((X - xv)**2 + (Y - yv)**2)
    v = - strength / (2 * np.pi) * (X - xv) / ((X - xv)**2 + (Y - yv)**2)
    return u, v

def get_stream_function_vortex(strength, xv, yv, X, Y):

    psi = strength / (4 * np.pi) * np.log((X - xv)**2 + (Y - yv)**2)

    return psi

# computes the velocity field on the mesh grid
u_vortex, v_vortex = get_velocity_vortex(gamma, x_vortex, y_vortex, X, Y)

# computes the stream-function on the mesh grid
psi_vortex = get_stream_function_vortex(gamma, x_vortex, y_vortex, X, Y)

# superposition of the doublet and the vortex on the freestream flow
u = u_freestream + u_doublet + u_vortex
v = v_freestream + v_doublet + v_vortex
psi = psi_freestream + psi_doublet + psi_vortex

# calculates cylinder radius
R = np.sqrt(kappa / (2 * np.pi * u_inf))

# calculates the stagnation points
x_stagn1, y_stagn1 = + \
    np.sqrt(R**2 - (gamma / (4 * np.pi * u_inf))**2), - \
    gamma / (4 * np.pi * u_inf)
x_stagn2, y_stagn2 = - \
    np.sqrt(R**2 - (gamma / (4 * np.pi * u_inf))**2), - \
    gamma / (4 * np.pi * u_inf)

# plots the streamlines
size = 10
plt.figure(figsize=(size, (y_end - y_start) / (x_end - x_start) * size))
plt.xlabel('x', fontsize=16)
plt.ylabel('y', fontsize=16)
plt.xlim(x_start, x_end)
plt.ylim(y_start, y_end)
plt.streamplot(X, Y, u, v, density=2, linewidth=1,
               arrowsize=1.5, arrowstyle='->')
circle = plt.Circle((0, 0), radius=R, color='#CD2305', alpha=0.5)
plt.gca().add_patch(circle)
plt.scatter(x_vortex, y_vortex, color='#CD2305', s=80, marker='o')
plt.scatter([x_stagn1, x_stagn2], [y_stagn1, y_stagn2],
            color='g', s=80, marker='o')

plt.show()

# calculates the surface tangential velocity on the cylinder
theta = np.linspace(0, 2 * np.pi, 100)
u_theta = -2 * u_inf * np.sin(theta) - gamma / (2 * np.pi * R)

# computes the surface pressure coefficient
cp = 1.0 - (u_theta / u_inf)**2

# if there was no vortex
u_theta_no_vortex = -2 * u_inf * np.sin(theta)
cp_no_vortex = 1.0 - (u_theta_no_vortex / u_inf)**2

# plots the surface pressure coefficient
size = 6
plt.figure(figsize=(size, size))
plt.grid(True)
plt.xlabel(r'$\theta$', fontsize=18)
plt.ylabel(r'$C_p$', fontsize=18)
plt.xlim(theta.min(), theta.max())
plt.plot(theta, cp, color='#CD2305', linewidth=2, linestyle='-')
plt.plot(theta, cp_no_vortex, color='g', linewidth=2, linestyle='-')
plt.legend(['with vortex', 'without vortex'], loc='best', prop={'size': 16})

plt.show()