#Investigating asymmetry in particle collisions

import scipy as sp
import numpy as np
from scipy import *
from numpy import *
from matplotlib import pyplot as plt
import scipy.optimize as spo

#Task 1
print "Task 1"
# PART 1
# plotting the functions of sigmaA and sigmaB as a function of COM energy

# step 1; writing out some predefined parameters to minimize mistypes
k=1000000.0    #constant of the collider
Mz=91.0        #mass of the Z boson in GeV
Gz=2.5        #width of Z boson defined as per equation
rs=0.14      #the parameter r describes the measurements at peak of cross-section for Z production
js=-0.033    #j parameters describe energy dependance of total and forward and backward cross-sections
ra=0.0027
ja=0.81

# step 2; defining the functions sigmaA and sigmaS with the squared COM
#energy as its argument
def sigS(S):
    result= ((4/3)*pi)*((1/S)+(((S*rs)+(S-Mz**2)*-js)/((S-Mz**2)**2+((Mz**2)*(Gz**2)))))
    return result

def sigA(S):
    result2= (pi)*(((S*ra)+(S-Mz**2)*ja)/((S-Mz**2)**2+((Mz**2)*(Gz**2))))
    return result2

#step 3; a linear space of COM energies within the accelerators range (20-140GeV),was created-
e = np.linspace(20, 140, 100)
# squaring all the COM energies to get S values,
s = e*e

#step 4; the function SigmaS was performed on the numerical array S,
# and the results were labelled B
B = sigS(s)
print "COM energies squared- s values", s
print "SigS(s)", B

#step 5; the function SigmaA was performed on the numerical array S,
# and the results were labelled C
C= sigA(s)
print "COM energies sqaured- s values", s
print "SigA(s)", C

# step 6; plot the functions SigmaA(s) and SigmaS(s)
# against the COM energies, e
#plt.plot(e, B, 'r-')
#plt.plot (e, C, 'b-')
#plt.legend(('sigma-S', 'sigma-A'),loc='upper right')
#plt.xlabel("COM energy/ GeV")
#plt.ylabel("Sigma-A and Sigma-S values/ GeV^-2")
#plt.show()
#plt.savefig("task1plot1.png")
#plt.clf()

# PART 2
# Plotting of Total Muon Pairs Produced per Day as
# a Function of COM Energy

# step 1; the expression for DNu/Dcos(theta)
# was integrated between -0.95 and 0.95 (the range of angles),
#in order to obtain an expression for the number of muon pairs as a function of COM energy

#step 2; the function for Number of muon pairs
# with the argument, s (COM energy squared) is defined in python
def Nu(x):
    result=2*1000000*sigS(x)*((0.95)+((0.95**3)/3))
    return result

#step 3; the function was carried out on the predefined s values
D= Nu(s)
print s
print D

# step 4; the aqcuired expression for of Nu as a function of
# COM energy was plotted.

plt.plot(e, D, 'r-')
plt.xlabel("COM energy / GeV")
plt.ylabel("Number of muon pairs produced")
plt.show()
plt.savefig("task1plot2.png")
plt.clf()

#Task 2
print "Task 2"
#simulating the number of muons detected per cos theta bin for an accelerator
#operating at a fixed COM energy.
#step 1; fix COM energy as 90.0 GeV, then s=8100 GeV^2
sqrtS =90
sfixed=sqrtS**2

#step 2; calculate sigma A and sigma S parameters for s=8100 GeV^2
SS= sigS(sfixed)
print "Sigma-S values", SS
SA= sigA(sfixed)
print "Sigma-A values", SA

#step 3; set dcos(theta) as 0.01
#separate the data points into cos theta bins of width 0.01
dt= 0.01
cost = np.arange(-0.95, 0.95, dt)

#step 4; define the expression for the Expected value of muon pairs per
#cos theta bin
#this is DNu/Dcos(theta)

def dNu(theta):
    result3 =dt*k*(SS*(1+theta**2)+SA*theta)
    return result3

#step 5; calculate expected number of muons per cos theta bin
D= dNu(cost)
print "cos theta values", cost
print "expected number of muon pairs", D

#step 6; plot expected number of muons as a function of cos theta
plt.plot(cost, D, 'r-')
plt.xlabel("cos theta")
plt.ylabel("Expected number of muon pairs")
plt.show()
plt.savefig("task2plot1.png")

#step 7; find the number of data points in D
N= len(D)

#step 8; smear using a Poisson random number generator,
#with mean D, and the number of trials as the number of D values
E = np.random.poisson(D,N)
print "smeared data with poisson noise", E

#step 9; plot the smeared data as a histogram.
#The red line is the expected value, D

plt.bar(cost,E,width=1e-2)
plt.plot(cost, D, 'r-')
plt.ylabel("Number of muon pairs")
plt.legend(('expected value', 'smeared data'),loc='upper right')
plt.show()
plt.savefig("task2plot2.png")
plt.clf()

#step 10; calculate the error in the experimental values
#this is a counting experiment following Poisson distribution
#thus the error= square root of counts for each interval

err=(E)**0.5
print "Error", err

#step 11; Add these as error bars to the graph
plt.plot(cost, E, 'b-')
plt.xlabel("cos theta")
plt.ylabel("(Experimental) Number of muon pairs")
plt.errorbar(cost, E, xerr=0, yerr= err)
plt.show()
plt.savefig("task2plot3.png")
plt.clf()

#Task 3
print "Task 3"

#Part 1- plotting the actual ratios of SigmaA/SigmaS for comparison
e=np.linspace(20, 140, 190)
s=e*e
SS= sigS(s)
SA= sigA(s)
ratio1= (SA/SS)

plt.plot(e, ratio1, 'r-')
plt.xlabel("Center of Mass Energies/ GeV")
plt.ylabel("ratio of Sigma-A/Sigma-S")
plt.show()
plt.savefig("task3plot1.png")
plt.clf()

#Part 2- fitting the ratio and plotting as a function of COM energies and bin widths
#step 1; define function for data fitting-
def fitfunc(x,a,s):
    func= k*(s*(1+x**2)+a*x)
    return func
#step 2; create array of ratios
Aratios= np.array([], dtype=float)
Aerr=np.array([], dtype=float)

#step 3;create loop that goes through values of e and fits for the ratio
for q in e:
    s=q*q
    SS=sigS(s)
    SA= sigA(s)
    D=dNu(cost)
    E=np.random.poisson(D,len(D))
    err=(E)**0.5
    x=np.arange(-0.95, 0.95, dt)
    y= np.array([E], dtype=np.float)
    y_err= err
    initial_guess=[SA,SS]
    po,po_cov= spo.curve_fit(fitfunc,cost,E,initial_guess,y_err)
    ratio2 =po[0]/po[1]
    rerr= sp.sqrt(((po_cov[0,0])/po[0])**2+((po_cov[1,1])/po[1])**2) #adding errors in quadrature to get error in ratio
    Aratios= np.append(Aratios, ratio2, axis=None)
    Aerr= np.append(Aerr, rerr, axis=None)

#step 4;plot
plt.plot(e,Aratios, 'b-')
plt.errorbar(e,Aratios, xerr=0, yerr= Aerr)
plt.xlabel("Center of Mass Energies/ GeV")
plt.ylabel("ratio of Sigma-A/Sigma-S")
plt.show()
plt.savefig("task3plot2.png")
plt.clf()

#step5; effect of bin width on ratio
#fix COM energy
sqrtS= 90.0
s= sqrtS**2

#step 6; calculating simulated data with poisson noise
def dNu(dt,theta):
    result3 =dt*k*(SS*(1+theta**2)+SA*theta)
    return result3

dt=np.array([0.01,0.05,0.1])
for x in dt:
    print "bin width=", x
    cost = np.arange(-0.95, 0.95, x)
    D= dNu(x,cost)
    E=np.random.poisson(D,len(D))
    err=(E)**0.5

#step 7; define function for data fitting-
    def fitfunc(x,a,s):
        func= k*(s*(1+x**2)+a*x)
        return func
    x=np.arange(-0.95, 0.95, x)
    y= np.array([E], dtype=np.float)
    y_err= err

#step 8; make an initial guess and use curve fitting function
# to guess parameters and their uncertainties
    initial_guess=[-0.004637,0.042374]
    po,po_cov= spo.curve_fit(fitfunc,cost,E,initial_guess,y_err)

    #step 9; print parameters and uncertainties
    print "Values with errors;"
    print "sigma a=", po[0], "+/-", sp.sqrt(po_cov[0,0])
    print "sigma s=", po[1], "+/-", sp.sqrt(po_cov[1,1])

    #step 10; calulate experimental ratio of SigmaA/Sigma S
    ratio2 =po[0]/po[1]
    rerr= sp.sqrt(((po_cov[0,0])/po[0])**2+((po_cov[1,1])/po[1])**2)
    print "ratio from fitted results", ratio2
    print "uncertainty in ratio", rerr

import numpy,from numpy,import scipy,from scipy


#T# repeating decimals can be converted into fractions, and when starting with a fraction, the repeating part of its repeating decimal can be found if the fraction is a repeating decimal

#T# to work with repeating decimals and fractions, the sympy package is used
import sympy

#T# create a repeating decimal, any repeating decimal has two parts, an unique part (which may be zero), and a repeating part
num1 = 894.57    #| unique part of the repeating decimal number
num2 = 448132653 #| repeating part of the repeating decimal number
#| the repeating decimal itself is 894.57448132653448132653448132653448132653

#T# get the amount of digits after decimal point in the unique number
num1_1 = len(str(num1).split('.')[1]) # 2

#T# get the amount of digits in the repeated number
num2_1 = len(str(num2))               # 9

#T# the calculation of the fraction is based on subtracting the repeating part of the number to get rid of it, and for this, two multiples of the number are created and the smaller is subtracted from the larger one, after this the fraction can be found

#T# calculate the numerator, it's all the unique digits and one time the repeated digits, both put to the left of the decimal point, minus the unique digits also put to the left of the decimal point
num3 = int(num1*10**(num1_1 + num2_1) + num2 - num1*10**num1_1)
# 89457448132653 - 89457 == 89457448043196

#T# calculate the denominator, it's the subtraction of the powers of ten that were needed to create the numerator
num4 = int(10**(num1_1 + num2_1) - 10**num1_1)
# 100000000000 - 100 == 99999999900

#T# create a fraction with the numerator and denominator, using the Rational constructor
num5 = sympy.Rational(num3, num4) # 2484929112311/2777777775

#T# now to do the opposite process, long division is used to calculate the remainder and quotient in iterations, once a remainder repeats itself in the iterations, that means that the repeating part of the decimal number is found

#T# create a list to store the quotients, and one to store the remainders of the iterations
list1 = []
list2 = []

#T# create the numerator and the denominator of the fraction whose repeating part wants to be found
num1 = 2484929112311
num2 = 2777777775

#T# create the loop to do the long division
bool1 = False
while bool1 == False:
    num3 = num1//num2 #| quotient of the iteration
    num4 = num1%num2  #| remainder of the iteration
    if num4 in list2:
        bool1 = True
    list1.append(num3)
    list2.append(num4)
    num1 = 10*num4    #| dividend for the next iteration

#T# list1 contains the quotients of each iteration, and list2 the remainders of each iteration
list1 # [894, 5, 7, 4, 4, 8, 1, 3, 2, 6, 5, 3]
list2 # [1595781461, 2068925735, 1244812925, 1337018150, 2259070400, 368481800, 907040225, 737068925, 1815133700, 1484670350, 957814625, 1244812925] #| the remainder 1244812925 is repeated at index 2 and at the last element

#T# get the index with the first occurrence of the remainder, the second occurrence is always the last element in list2
num5 = list2.index(num4) # 2

#T# the index is augmented by 1 to avoid repeating the remainder, because num5 is supposed to signal the index from which all remainders are unique
num5 += 1 # 3

#T# cast the elements in list1 into a list of strings, to concatenate the numbers
list3 = [str(it1) for it1 in list1]

#T# the result is shown as int1.unique1(repeating1) where int1 is the integer part, unique1 is the unique part of the decimals, and repeating1 is inside parentheses to indicate the repeating part of the decimal number
str1 = list3[0] + '.' + ''.join(list3[1:num5]) + '(' + ''.join(list3[num5:]) + ')' # '894.57(448132653)'


# -*- coding: utf-8 -*-
import numpy as np

def bgd(X, Y, alpha=0.01, epsilon=1e-8, trace=True):
    m = len(X)
    _X = np.column_stack((np.ones(m), X))
    m, n = np.shape(_X)
    theta, sse2, cnt = np.ones(n), 0, 0
    Xt = _X.T
    while True:
        loss = np.dot(_X, theta) - Y
        # sse = np.sum(loss ** 2) / (2 * m)
        # sse = loss.T.dot(loss) / (2 * m)
        sse = np.dot(loss.T, loss) / (2 * m)
        if abs(sse - sse2) < epsilon:
            break
        else:
            sse2 = sse

        if trace:
            print("[ Epoch {0} ] theta = {1}, loss = {2}, error = {3})".format(cnt, theta, loss, sse))

        gradient = np.dot(Xt, loss) / m
        theta -= alpha * gradient
        cnt += 1
    return theta

if __name__ == '__main__':
    import matplotlib.pyplot as plt

    X = [1, 2, 3, 4, 5, 6, 7, 8, 9]
    Y = [1.99, 3.89, 5.80, 7.83, 9.80, 11.77, 13.80, 15.75, 17.71]

    b, a = bgd(X, Y, 0.05, 1e-6)

    print('y = {0} * x + {1}'.format(a, b))

    x = np.array(X)
    plt.plot(x, Y, 'o', label='Original data', markersize=5)
    plt.plot(x, a * x + b, 'r', label='Fitted line')
    plt.show()

    x = [(0., 3), (1., 3), (2., 3), (3., 2), (4., 4)]
    y = [95.364, 97.217205, 75.195834, 60.105519, 49.342380]

    _theta = bgd(x, y, 0.1, 1e-8)
    print('theta = {0}'.format(_theta))


import numpy as np

'''
Training
For each target value vj:
    P'(vj) = estimate P(vj)
    for each attribute value ai of each attribute a:
        P(ai | vj) = estimate P(ai | vj)

Classifying
vnb = argmax[vj in V] P(vj) * PRODUCT(ai in x) P(ai | vj)
'''

def naive_bayes_learn(examples):
    target_classes = set([ ex[-1] for ex in examples ]) # get the different target values
    print("target classes\n", target_classes)

    # For each target value vj
    target_probabilities = {}
    for target_value in target_classes:
        number_of_occurencens = 0
        for ex in examples:
            if ex[-1] == target_value:
                number_of_occurencens += 1
        # P(vj) = estimate p(vj)
        target_probabilities[target_value] = number_of_occurencens / len(examples)
    print("target probabilities\n", target_probabilities)

    # for each attribute value ai of each attribute a
    attribute_values = {} # {ai = [v1, v2, ,..,vn]}
    for ex in examples:
        ex = ex[:-1] # remove target from example
        for ai_idx, ai in enumerate(ex):
            attribute_value = attribute_values.get(ai_idx, [])
            attribute_value.append(ai)
            attribute_values[ai_idx] = attribute_value
    for ai in attribute_values:
        attribute_values[ai] = list(set(attribute_values.get(ai, []))) # remove duplicates
    print("attribute values\n", attribute_values)

    # P(ai | vj) = estimate p(ai |vj) # count how many ai with class vj over total examples
    attribute_target_probability = {}
    for a in attribute_values:
        for ai in attribute_values[a]:
            for vj in target_classes:
                count = 0
                for ex in examples:
                    if ex[a] == ai and ex[-1] == vj:
                        count += 1
                #    P(ai | vj)   =    P(ai ^ vj)            /     p(vj)
                probability_ai_vj = (count / len(examples)) / target_probabilities[vj]
                key = str(a)+"."+str(ai)+"|"+str(vj)
                attribute_target_probability[key] = probability_ai_vj
    print("Attribute_target_probability: \n", attribute_target_probability)

    return target_classes, target_probabilities, attribute_target_probability

def naive_bayes_classify_new_instance(x, target_classes, target_probs, ai_target_probs):
    probs = {}
    for vj in target_classes:
        p = None
        for a, ai in enumerate(x[:-1]):
            key = str(a)+"."+str(ai)+"|"+str(vj)
            if p == None:
                p = ai_target_probs[key]
            else:
                p *= ai_target_probs[key]
        p *= target_probs[vj]
        probs[vj] = p

    print("New case probability\n", probs)
    return max(probs)

if __name__ == "__main__":
    data = np.loadtxt("data/data.txt", dtype=str, comments="#", delimiter=", ")
    tc, tp, atp  = naive_bayes_learn(data)

    new_case = ["overcast", "true", "low", "yes"]
    target = naive_bayes_classify_new_instance(new_case, tc, tp, atp)
    print("Prediction is: ", target)


# Newton root finding methods tests

from math import sin, cos, exp, log

from newton import newton, newton_aitken, newton_chord, newton_secant
import numpy as np

# Get machine precision
eps = np.finfo(float).eps

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

print('-----------------------------------------------------------------------')
print(' f(x) = 2^x-cos(x+3)^2+exp(x+2)')
f  = lambda x: 2**x-(cos(x+3))**2+exp(x+2)
f1 = lambda x: 2**x*log(2)+2*cos(x+3)*sin(x+3)+exp(x+2)
print('> Newton plain')
x, steps = newton(0, f, f1, eps, 1000, 1)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))
print('> Newton chords')
x, steps = newton_chord(0, f, f1, eps, 1000)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))
print('> Newton secant')
x, steps = newton_secant(0, f, f1, eps, 1000)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))

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

print('-----------------------------------------------------------------------')
print(' f(x) = log(sin(x+4)-cos(x-1))-exp(3*x-x^2)')
f  = lambda x: log(sin(x+4)-cos(x-1))-exp(3*x-x**2)
f1 = lambda x: (cos(x+4)+sin(x-1))/(sin(x+4)-cos(x-1))-exp(3*x-x**2)*(3-2*x)
print('> Newton plain')
x, steps = newton(4, f, f1, eps, 100, 1)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))

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

print('-----------------------------------------------------------------------')
# 90 iterations to converge... too many for a quadratic method,
# Maybe the root has multiplicity > 1?
print(' f(x) = (4+log(x))*x*(log(x))^3')
f  = lambda x: (4+log(x))*x*(log(x))**3
f1 = lambda x: (log(x)**2)*((log(x)**2)+8*log(x)+12)
print('> Plain Newton')
x, steps = newton(4, f, f1, eps, 100, 1)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))
# With Aitken acceleration: 5 iterations
print('> Aitken acceleration')
x, steps = newton_aitken(4, f, f1, eps, 100)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))
# The root multiplicity should be 3, lets test the plain Newton method with
# known root multiplicity: 6 iterations
print('> Passing the root multiplicity to plain Newton (m=3)')
x, steps = newton(4, f, f1, eps, 100, 3)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))
# Quasi-Newton method: chords
print('> Newton chords')
x, steps = newton_chord(4, f, f1, eps, 5000)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))
# Quasi-Newton method: secant
print('> Newton secant')
x, steps = newton_secant(4, f, f1, eps, 5000)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))

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

print('-----------------------------------------------------------------------')
# The following function root computation with the plain Newton method is
# very inefficient. With 5000 steps the method converges with an error ~5E-7.
# Indeed the root has infinite multiplicity.
print('> Infinite multiplicity root')
print(' f(x) = sin(x-1)-0.5*sin(2*(x-1))')
f  = lambda x: sin(x-1)-0.5*sin(2*(x-1))
f1 = lambda x: cos(x-1)-0.5*x*cos(2*(x-1))
print('> Newton plain')
x, steps = newton(3, f, f1, eps, 5000, 1)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))
# Same experiment but with Aitken acceleration
print('> Aitken acceleration')
x, steps = newton_aitken(3, f, f1, eps, 5000)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))
# Quasi-Newton method: chord
print('> Newton chord')
x, steps = newton_chord(3, f, f1, eps, 5000)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))
# Quasi-Newton method: secant
print('> Newton secant')
x, steps = newton_secant(3, f, f1, eps, 5000)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))

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

print('-----------------------------------------------------------------------')
# Even with a not so small error tolerance the plain Newton method takes too
# many steps to converge.
print(' f(x) = (3*x-9)^7')
f  = lambda x: (3*x-9)**7
f1 = lambda x: 7*x*(3*x-9)**6
print('> Newton plain')
x, steps = newton(8, f, f1, 1e-11, 300, 1)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))
# The root multiplicity is clearly 7
print('> Passing the root multiplicity to plain Newton (m=7)')
x, steps = newton(8, f, f1, 1e-11, 300, 7)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))
# Quasi-Newton method: chord
print('> Newton chord')
x, steps = newton_chord(8, f, f1, 1e-11, 5000)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))
# Quasi-Newton method: secant
print('> Newton secant')
x, steps = newton_secant(8, f, f1, 1e-11, 5000)
print(" x = {}\n steps = {}\n error = {}".format(x, steps, abs(f(x))))


# Python function that calculates the local extreme point of curvature (vortex)
# point in a specified interval of a unimodal function.
# Simply change the maxF variable to the function
# you'd like to test and set the parameters of max_grs.
# ---WORKS ONLY FOR ONE VARIABLE FUNCTIONS----
# ---  IT IS RECOMMENDED TO  USE 'x' AS THE VARIABLE---
"""
# References
https://en.wikipedia.org/wiki/Golden-section_search
https://en.wikipedia.org/wiki/Vertex
"""

import numpy as np
from matplotlib import pyplot as plt  # import module to plot the result

target_function = (
    lambda x: x ** 3 - 6 * x ** 2 + 4 * x + 12
)  # type the function of interest here

def max_golden_search(
    func_to_max, upper: float, lower: float, tol: float = 1e-8
) -> float:

    """
    Function in python that calculates the maximum
    point of function in a specified interval
        :param func_to_max : the function to maximize (target_function)
        :param upper: upper boundary of interval
        :param lower: lower boundary of interval
        :param tol: set an appropriate tolerance value
        :returns maximum of specified interval
    >>> max_golden_search(target_function, -2, 4)
    count = 43
    0.3670068499227286
    >>> max_golden_search(target_function, 'm', 4)
    Traceback (most recent call last):
            ...
    ValueError: give numerical values for upper, lower limits
    """

    if type(upper) is str or type(lower) is str:
        raise ValueError("give numerical values for upper, lower limits")

    inv_gr = (np.sqrt(5) - 1) * 0.5  # ~ 0.618, this is the inverse golden ratio
    d_max = (upper - lower) * inv_gr  # # Sets initial value for d
    q1 = lower + d_max  # Sets initial value for q1
    q2 = upper - d_max  # and q2

    count = 0  # Creates a count variable with initial value zero

    while (
        abs(upper - lower) > tol
    ):  # Sets the tolerance condition. Will stop running once |u -l| < tol

        if func_to_max(q1) > func_to_max(q2):
            lower = q2
        else:
            upper = q1
        d_max = (upper - lower) * inv_gr
        q1 = lower + d_max
        q2 = upper - d_max
        count += 1  # Adds one to the count per iteration

    print("count =", count)  # Prints the count number

    return (q1 + q2) * 0.5  # Returns the midpoint of the
    # final interval as the maximum point in the range

max_point = max_golden_search(target_function, -2, 4)

def plot_point(
    maximum_point: float = max_point,
    x_axis_linspace: np.linspace = np.linspace(-5, 5, 200),
) -> None:
    """
    Plots function and maximum point
    """
    fmt = "bo"  # format string for the color of the point
    plt.plot(x_axis_linspace, target_function(x_axis_linspace))
    plt.plot(maximum_point, target_function(maximum_point), fmt)
    plt.show()

if __name__ == "__main__":

    import doctest

    doctest.testmod()


import numpy as np
import sympy as sp
import typing
import vorpy.symbolic

def directional_derivative (
    V:np.ndarray,
    f:typing.Any,
    X:np.ndarray,
    *,
    post_process_o:typing.Optional[typing.Callable[[typing.Any],typing.Any]]=sp.simplify,
) -> np.ndarray:
    """
    This returns the directional derivative of f along V in coordinates X.

    If post_process_o is not None, will call post_process_o on each component of the result.
    The default for post_process_o is sympy.simplify.
    """
    if V.shape != X.shape:
        raise TypeError(f'expected vector field V to have the same shape as coordinates X, but V.shape = {V.shape} and X.shape = {X.shape}')

    V_flat = V.reshape(-1)
    f_flat = np.reshape(f, -1)
    X_flat = X.reshape(-1)
    df_flat = vorpy.symbolic.differential(f_flat, X_flat)
    assert df_flat.shape == f_flat.shape + X_flat.shape
    V_dot_df = np.dot(df_flat, V_flat).reshape(np.shape(f))

    # For some reason, the result of sp.simplify(V_dot_df) is class
    # 'sympy.tensor.array.dense_ndim_array.ImmutableDenseNDimArray'
    # so instead call sp.simplify on each element of the result so
    # that it is a numpy.ndarray.  If the order of the result is
    # zero, then extract the single, scalar element.
    if post_process_o is not None:
        V_dot_df = np.vectorize(post_process_o)(V_dot_df)

    # TODO: test on scalar expressions f, as this [()] expression may not work in that case
    if V_dot_df.shape == ():
        return V_dot_df[()]
    else:
        return V_dot_df

def lie_bracket (A, B, X):
    """
    Compute the Lie bracket of vector fields A and B with respect to coordinates X.

    A formula for [A,B], i.e. the Lie bracket of vector fields A and B, is

        J__B_reshaped*A - J__A_reshaped*B

    where J_V is the Jacobian matrix of the vector field V.

    See https://en.wikipedia.org/wiki/Lie_bracket_of_vector_fields#In_coordinates
    See https://en.wikipedia.org/wiki/Jacobian_matrix_and_determinant
    """

    if A.shape != B.shape or A.shape != X.shape:
        raise TypeError(f'The shapes of A, B, and X must all be equal, but instead got A.shape = {A.shape}, B.shape = {B.shape}, X.shape = {X.shape}')

    # Flatten the shapes out for computing and contracting the Jacobian matrices.
    A_reshaped = A.reshape(-1)
    B_reshaped = B.reshape(-1)
    X_reshaped = X.reshape(-1)
    # Compute the Jacobian matrices of (the reshaped versions of) A and B
    J__A_reshaped = vorpy.symbolic.differential(A_reshaped, X_reshaped)
    J__B_reshaped = vorpy.symbolic.differential(B_reshaped, X_reshaped)
    # Reshape the result into the original shape of the vector fields and coordinates.
    return (np.dot(J__B_reshaped,A_reshaped) - np.dot(J__A_reshaped,B_reshaped)).reshape(X.shape)


import numpy as np

a.size
a.itemsize
a.size * a.itemsize

a = np.arange(10,50)

a = a[::-1]

a = np.arange(0,9).reshape(3,3)

np.nonzero(a)

np.eye(3)

print(a.min(), a.max())

print(a.mean())

a = np.zeros((10,10))
a = np.pad(a, 1, constant_values=1)

a = np.ones((10,10))
a[1:-1,1:-1] = 0

np.diag([1,2,3,4], -1)

a[::2,1::2] = 1
a[1::2,::2] = 1

np.unravel_index(99, (6,7,8))

np.tile([[1,0],[0,1]], (4,4))

z = (a - a.mean())/a.std()

np.dtype([("r", np.ubyte),("g", np.ubyte),("b", np.ubyte),("a", np.ubyte)])

a = np.random.rand(5,3)
b = np.random.rand(3,2)
a @ b

a[(a > 3) & (a < 8)] *= -1

np.where(a > 0, np.ceil(a), np.floor(a))

np.sqrt(-1)
np.emath.sqrt(-1)

date = np.datetime64
now = date('today')
np.timedelta64(1, 'D') + now
np.timedelta64(-1, 'D') + now
np.arange('2016-07', '2016-08', dtype='datetime64[D]')

np.trunc(a)

np.tile(np.arange(0,5),(5,1))

np.fromiter(range(10), int)

np.linspace(0,1,10)

a.sort()

np.add.reduce(a)

np.allclose(a,b)
np.array_equal(a,b)

a[a.argmax()] = 0

Z = np.zeros((5,5), [('x',float),('y',float)])
Z['x'], Z['y'] = np.meshgrid(np.linspace(0,1,5),
                             np.linspace(0,1,5))
print(Z)

np.subtract.outer(x,y)

arr = np.add.outer(x,y)

Z = np.zeros(10, [ ('position', [ ('x', float, 1),
                                  ('y', float, 1)]),
                   ('color',    [ ('r', float, 1),
                                  ('g', float, 1),
                                  ('b', float, 1)])])

Z[0]['position']['x']

import scipy.spatial
scipy.spatial.distance.cdist(x,x)

X,Y = np.atleast_2d(x[:,0], x[:,1])
np.sqrt((X-X.T)**2 + (Y-Y.T)**2)

a[:] = a.view(int)

from io import StringIO

s = StringIO("""1, 2, 3, 4, 5
6,  ,  , 7, 8
 ,  , 9,10,11""")

np.genfromtxt(s,delimiter=',', filling_values=0)

list(np.ndenumerate(np.random.rand(3,4)))
list(np.ndindex((3,4)))

a - a.mean(1).reshape(-1,1)
a - a.mean(1,keepdims=True)

a.sum((-1,-2))

np.diag(A @ B)
np.einsum('ij, ji -> i',A,B)  # diagonal

np.einsum('ij, ji',A,B)  # Trace of result


import random as r
import matplotlib.pyplot as plt
import numpy as np
import numpy.linalg as linalg
import math
def in_1d_boundary(position, boundary):
    return position > boundary[0] and position < boundary[1]

def run_1d_trial(steps, boundary):
    """
    steps: number of steps to take
    boundary: set of points that consititute a boundary (doesn't make sense with more than 2)

    Runs trial for one dimension starting from 0 WLOG since we have control over the boundary.
    Returns a map from square -> number of times we reached that square
    """
    starting_point = 0
    seen_squares = {starting_point: 1} #includes starting square
    take_step = lambda : (-2) *(r.random() < 0.5) + 1 # take a step left (-1) with p = 1/2 and right (+ 1) with p = 1/2
    current_sq = starting_point
    for step in range(steps):
        step_vector = take_step()
        current_sq += step_vector
        if not in_1d_boundary(current_sq, boundary):
            break
        seen_squares[current_sq] = seen_squares.setdefault(current_sq, 0) + 1 #increase square count by 1

    return current_sq, seen_squares

def combine_freq_dists(f1, f2):
    new_f = {}
    for coord in f1:
        new_f[coord] = new_f.setdefault(coord, 0) + f1[coord]

    for coord in f2:
        new_f[coord] = new_f.setdefault(coord, 0) + f2[coord]

    return new_f

def visualize_data(results):
    exit_points = {}

    total_frequencies = {}
    for i, r in enumerate(results):
        exit_point, freq_dist = r
        exit_points[exit_point] = exit_points.setdefault(exit_point, 0) + 1
        total_frequencies = combine_freq_dists(total_frequencies, freq_dist)

    x_coords = []
    freqs = []
    for f in total_frequencies:
        x_coords.append(f)
        freqs.append(total_frequencies[f])

    plt.plot(x_coords, freqs)
def run_sim(trials, steps, boundary):
    """
    trials: Number of trials to run in the simulation
    steps: Number of steps to run per trial
    """
    trial_results = []
    for trial in range(trials):
        trial_results.append(run_1d_trial(steps, boundary))
        print(f"Trial {trial}: ", run_1d_trial(steps, boundary))
    # post processing of results
    visualize_data(trial_results)

def markov_processes():
    n = 9
    m = np.zeros((n, n))
    m[0, 1] = .5
    m[n-1, n-2] = .5
    for i in range(1, n-1):
        m[i, i-1] = .5
        m[i, i+1] = .5
    print(m)
    p_m = linalg.matrix_power(m, 10000)
    np.set_printoptions(precision=4)
    print(p_m)
    v = np.zeros(n)
    v[n//2] = 1
    res = p_m@v
    print(res)
    print("end probability: ", res[n//2] )

def what_step_exit(s, b):
    pos = 0
    for i in range(0, len(s), 2):
        if s[i:i+2] == '00':
            pos += 1
        elif s[i:i+2] == '01':
            pos -= 1
        if pos >= b:
            return i + 1
    return len(s)

def out_before(s, b):
    # 00 -> u 01 -> d,10 -> l, 01 -> r
    pos = 0
    for i in range(0, len(s)-2, 2):
        if s[i:i+2] == '00':
            pos += 1
        elif s[i:i+2] == '01':
            pos -= 1

        if pos >= b:
            return True

    return False

def out_at_end(s, b):
    pos = 0
    for i in range(0, len(s), 2):
        if s[i:i+2] == '00':
            pos += 1
        elif s[i:i+2] == '01':
            pos -= 1
    return pos >= b

def counting(s, b):
    # a word is a binary number. 0 represents down, 1 represents up
    words = []
    for i in range(4**s):
        words.append(format(i, f"0{2*s}b"))

    #print(words)
    #eliminate all words that would exit before the final step
    valid_words = []
    double_check = {}
    for w in words:
        if not out_before(w, b):
            valid_words.append(w)

    #find all words that escape on the last step
    escaped_words = []
    for w in valid_words:
        if out_at_end(w, b):
            escaped_words.append(w)

    print(f"Valid Words {len(valid_words)} ")#, valid_words)
    print(f"Escaped Words {len(escaped_words)}")#, escaped_words)
    print(f"Probability Escaping on step {s}: ", len(escaped_words)/len(valid_words))

memo = {}
def compute_number_paths_out(s, d):
    """
    Computes number of paths out in exactly s steps, when the boundary is d steps away.

    Recurrence: C(s, d) = C(s-1, d-1) + 2C(s-1, d) + C(s-1, d+1)

    The recurrence is for a random walk in 2d, where the boundary
    is the half plane that is d steps away.

    Base Cases:
        C(s, d) = 0 if s < d // less steps than to boundary then obviously can't reach it
        C(s, d) = 0 if d = 0 // we have already hit the boundary and and there are steps left
        C(s, d) = 1 if s= d // there is only one path that exactly hits the boundary (going straight there)

    Using memoization should lead to a runtime of O(s^2)
    O(1) work on each state, O(s)O(d) states is an overcount since s < d requires no recursion
    """
    memo[(0, 0)] = 1
    for s_i in range(s+1):
        for d_i in range(d+s_i+1):
            if (s_i, d_i) not in memo:
                if (s_i < d_i):
                    memo[(s_i, d_i)] = 0
                elif d_i == 0:
                    memo[(s_i, d_i)] = 0
                elif s_i == d_i:
                    memo[(s_i, d_i)] = 1
                else:
                    memo[(s_i, d_i)] = memo[(s_i - 1, d_i -1)] + 2*memo[(s_i - 1, d_i)] + memo[(s_i - 1, d_i + 1)]
    return memo[(s, d)]
    # if (s, d) in memo:
    #     return memo[(s, d)]

    # if (d > s):
    #     return 0
    # if (d == 0):
    #     return 0
    # if (s == d):
    #     return 1

    # c = lambda x, y : compute_number_paths_out(x, y)
    # ans = c(s-1, d-1) + 2*c(s-1, d) + c(s-1, d+1)
    # memo[(s, d)] = ans
    # return ans

def nCr(n, r):
    # f = math.factorial
    return math.comb(n, r)#f(n)//(f(n-r)*f(r))

def closed_form_paths_out(s, d):
    #rn only works for d = 2
    #compute row (2(s-1)) column 2(s-1)/2 + 1 in pascals triangle
    # print("computing s, d", s, d)
    current_row = 2*(s-1)
    offset = d-1
    midpoint = current_row//2
    total_num_out_with_ob = nCr(current_row, midpoint + offset)
    # extra paths are the entry immeadiatly above and to the right (but for some reason pascals goes by 2)
    extra_paths = nCr(current_row, current_row//2 + offset +2) if (current_row >= midpoint + offset + 2 ) else 0
    # print(extra_paths)
    return total_num_out_with_ob - extra_paths

def correction(n, val):
    print(f"(n, val) {(n, val)}")
    if (n == 0):
        return val
    if (n==1):
        return 2*val
    return 2*correction(n-1, val) + ((n%2) + 1)*val

memo_tot_paths = {}
def compute_number_paths(s, d):
    """
    Computes total number of paths of length s, that do not exit early

    Recurrence: C(s, d) = C(s-1, d-1) + 2C(s-1, d) + C(s-1, d+1)

    The recurrence is for a random walk in 2d, where the boundary
    is the half plane that is d steps away.

    Base Cases:
        C(s, d) = (4)^s if s <= d // all paths are viable none will leave early
        C(s, d) = 0 if d = 0 // we have already hit the boundary and and there are steps left

    Using memoization should lead to a runtime of O(s^2)
    O(1) work on each state, O(s)O(d) states is an overcount since s < d requires no recursion
    """
    memo_tot_paths[(0, 0)] = 1
    for s_i in range(s+1):
        for d_i in range(d+s_i+1):
            if (s_i, d_i) not in memo_tot_paths:
                if (s_i <= d_i):
                    memo_tot_paths[(s_i, d_i)] = (4)**s_i
                elif d_i == 0:
                    memo_tot_paths[(s_i, d_i)] = 0
                else:
                    memo_tot_paths[(s_i, d_i)] = memo_tot_paths[(s_i - 1, d_i -1)] + 2*memo_tot_paths[(s_i - 1, d_i)] + memo_tot_paths[(s_i - 1, d_i + 1)]
    return memo_tot_paths[(s, d)]

def probability_leaving(s, d):
    total_probability = 0
    for i in range(1,s+1):
        # extra_paths = sum([compute_number_paths_out(j, d) for j in range(1, i-1)])
        total_probability += compute_number_paths_out(i, d)/1#(4**i)#(compute_number_paths(i, d) + extra_paths)
    return total_probability

def ev_half_plane(d, epochs):
    ev = 0
    end_epoch = d+epochs
    # print(sum([closed_form_paths_out(s, d)/4**s for s in range(d, end_epoch)]))
    for s in range(d, d+epochs+1):
        #extra_paths = sum([compute_number_paths_out(j, d) for j in range(1, s-1)])
        paths_out = closed_form_paths_out(s, d)
        total_paths = 4**s
        #print(f"greater: {paths_out < total_paths} s: {s} paths out: {paths_out} total_paths: {total_paths}")
        ev += (s*paths_out)/(total_paths)#(4**(end_epoch))

    return ev

def pascals_triangle(d):

    for i in range(d):
        row = ""
        for j in range(i+1):
            row += str(nCr(i, j)) + " "
        row = row[:-1]
        print(row)

import sys
if __name__ == "__main__":
    trials = int(sys.argv[1]) if len(sys.argv) > 1 else 10
    bound = 2
    pascals_triangle(20)
    # ev = ev_half_plane(bound, trials)
    # print(ev)
    # for steps in range(bound, bound+trials):
    #     cf_paths_out = closed_form_paths_out(steps, bound)
    #     num_paths_out = compute_number_paths_out(steps, bound)
    #     num_paths = compute_number_paths(steps, bound)
    #     prob_leave = probability_leaving(steps, bound)
    #     # counting(steps, bound)
    #     print(f"CF paths out: {cf_paths_out} Paths Out: {num_paths_out} diff {cf_paths_out - num_paths_out}")
    #     # print(f"Probability of leaving in under {steps} steps: {prob_leave}")

    #markov_processes()
    # b_1d = (float("-inf"), 7)
    # trials = 1
    # steps = 10
    # run_sim(trials, steps, b_1d)


# Import all libraries for this portion of the blog post
from scipy.integrate import quad
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

def print_normal_distribution():
    # print standard normal distribution

    x = np.linspace(-4, 4, num=100)
    constant = 1.0 / np.sqrt(2*np.pi)
    pdf_normal_distribution = constant * np.exp((-x**2) / 2.0)
    fig, ax = plt.subplots(figsize=(10, 5))
    ax.plot(x, pdf_normal_distribution)
    ax.set_ylim(0)
    ax.set_title('Standardized Normal Distribution', size=20)
    ax.set_ylabel('Probability Density', size=20)
    plt.show()

def normal_p_density(x):
    c = 1.0 / np.sqrt(2*np.pi)
    return c * np.exp((-x**2) / 2.0)

def calculate_p_from_z(z):
    """
    two tails
    """
    p1, _ = quad(normal_p_density, np.NINF, -z)
    p2, _ = quad(normal_p_density, z, np.Inf)
    return p1 + p2

def get_z_table():
    std_normal_table = pd.DataFrame(data=[],
                                    index=np.round(np.arange(0, 3.5, .1), 2),
                                    columns=np.round(np.arange(0.00, .1, 0.01), 2)
                                    )
    for i in std_normal_table.index:
        for c in std_normal_table.columns:
            z = np.round(i + c, 2)
            value, _ = quad(normal_p_density, np.NINF, z)
            std_normal_table.loc[i, c] = value

    std_normal_table.index = std_normal_table.index.astype(str)
    std_normal_table.columns = [str(column).ljust(4, '0') for column in std_normal_table.columns]
    return std_normal_table

if __name__ == "__main__":
    # print z distribution
    # print_normal_distribution()

    # calculate cumulative distribution
    # print(calculate_p_from_z(3), '\n')

    # print z-table for approximating  p-value by hand
    print(get_z_table().to_markdown())


# %%
"""In this code we wanna calculate the inverse of a matrix in numpy."""
import numpy as np
# %%
a = np.arange(1, 5).reshape((2, 2))
a
# %%
b = np.linalg.inv(a)
# %%
a.dot(b)
# %%
help(np.allclose)
# %%
np.eye(2)
# %%
np.allclose(a.dot(b), np.eye(2))
# %%
c = np.array([1, 2, 3, 4])
c
"""
"np.linalg.inv(c) will raise an error. for inversing a matrix in
it should be at least 2d.
"""
# %%
help(np.linalg.inv)
# %%
help(np.allclose)


import numpy as np

# CHANGEME:
setval = ["c_1", "c_2", "c_2"]
C = [[1, 0.3, 0.2], [0.3, 1, 0.1], [0.2, 0.1, 1]]
S = [0.2, 0.4, 0.3]

print("The correlation matrix C:")

print(f"     {'   '.join([str(i) for i in setval])}")
for rl, row in zip(setval, C):
    print('%s [%s]' % (rl, ' '.join('%04s' % i for i in row)))

print("- " * 10)

assert len(C) == 3
for i in C:
    assert len(i) == 3

inv_c = [3*[None] for _i in range(3)]

print("The Leibniz formula for the determinant of a 3 × 3 matrix:")
print("detC = c_11⋅(c_22⋅c_33 − c_32⋅c_23) − c_12⋅(c_21⋅c_33 − c_31⋅c_23) + c_13⋅(c_21⋅c_32 − c_31⋅c_22)\n")

det = (C[0][0] * ((C[1][1]*C[2][2]) - (C[2][1]*C[1][2]))) - (C[0][1] * ((C[1][0] *
                                                                         C[2][2]) - (C[2][0]*C[1][2]))) + (C[0][2] * ((C[1][0]*C[2][1]) - (C[2][0]*C[1][1])))
print(f"{C[0][0]} * ({C[1][1]}*{C[2][2]} - {C[2][1]}*{C[1][2]}) - ", end="")
print(f"{C[0][1]} * ({C[1][0]}*{C[2][2]} - {C[2][0]}*{C[1][2]}) + ", end="")
print(f"{C[0][2]} * ({C[1][0]}*{C[2][1]} - {C[2][0]}*{C[1][1]}) = ")

print(f"{C[0][0]} * ({C[1][1]*C[2][2]:.3f} - {C[2][1]*C[1][2]:.3f}) - ", end="")
print(f"{C[0][1]} * ({C[1][0]*C[2][2]:.3f} - {C[2][0]*C[1][2]:.3f}) + ", end="")
print(f"{C[0][2]} * ({C[1][0]*C[2][1]:.3f} - {C[2][0]*C[1][1]:.3f}) = ")

print(f"{C[0][0]} * ({(C[1][1]*C[2][2]) - (C[2][1]*C[1][2]):.3f}) - ", end="")
print(f"{C[0][1]} * ({(C[1][0]*C[2][2]) - (C[2][0]*C[1][2]):.3f}) + ", end="")
print(f"{C[0][2]} * ({(C[1][0]*C[2][1]) - (C[2][0]*C[1][1]):.3f}) = ")

print(f"{C[0][0] * ((C[1][1]*C[2][2]) - (C[2][1]*C[1][2])):.3f} - ", end="")
print(f"{C[0][1] * ((C[1][0]*C[2][2]) - (C[2][0]*C[1][2])):.3f} + ", end="")
print(f"{C[0][2] * ((C[1][0]*C[2][1]) - (C[2][0]*C[1][1])):.3f} = {det:.3f}")

print("- " * 10)

print(f"The invered correlation matrix at C_ij is (-1)^(i+j) * (new C_ij) / detC")
print(f"Example C_12 = (-1)^(1+2) * (C_21*C_33 - C_31*C23) / detC")

for i in range(3):
    for j in range(3):
        adj = [[n for ii, n in enumerate(row) if ii != i]
               for jj, row in enumerate(C) if jj != j]
        d = adj[0][0]*adj[1][1] - adj[0][1]*adj[1][0]
        print(
            f"C_{i+1}{j+1}\t= (-1)^({i+1}+{j+1}) * ({adj[0][0]}*{adj[1][1]} - {adj[0][1]}*{adj[1][0]}) / {det:.3f}")
        sgn = (-1)**(i+j)
        inv_c[i][j] = sgn * d / det
        print(
            f"\t= {'-' if (-1)**(i+j) < 0 else ''}{adj[0][0]*adj[1][1] - adj[0][1]*adj[1][0]:.3f} / {det:.3f} = {inv_c[i][j]:.3f}")

print("\nThe invered correlation matrix C:")

print(f"     {'     '.join([str(i) for i in setval])}")
for rl, row in zip(setval, inv_c):
    print('%s [%s]' % (rl, ' '.join(f"{i:.3f}" for i in row)))

print("- " * 10)

c = 0
for row in inv_c:
    if c == 1:
        print("SC^(-1)\t= ", end="")
    else:
        print(" \t  ", end="")
    print('[%s]' % (' '.join(f"{i:.3f}" for i in row)), end="")
    c+=1
    if c == 2:
        print(' * [%s] = ' % (' '.join(f"{i:.3f}" for i in S)))
    else:
        print()
print()
sc_inv = []
for i in range(len(inv_c[0])):  # this loops through columns of the matrix
    total = 0
    for j in range(len(S)):  # this loops through vector coordinates & rows of matrix
        total += S[j] * inv_c[j][i]
        print(f"({S[j]} * {inv_c[j][i]:.3f}) + ", end="")
    print(f" = {total:.3f}")
    sc_inv.append(total)
print()
print('SC^(-1) = [%s]' % (' '.join(f"{i:.3f}" for i in sc_inv)))
print("- " * 10)

c = 0
for row in S:
    if c == 1:
        print('SC^(-1) * S^T\t= ', end="")
    else:
        print("\t\t  ", end="")
    print(row, end="")
    c+=1
    if c == 2:
        print(' * [%s] = %f' % (' '.join(f"{i:.3f}" for i in sc_inv), np.array(S).T.dot(sc_inv)))
    else:
        print()


#Import stuff
import numpy as np
import matplotlib.pyplot as pp

a = 0.5 #A parameter defined in the problem statement

def initial_guess(x): #A function defining our initial guess
    return 1 + 2*x

def phi(x):  #A function defined in the problem statement
    return 20*np.pi*x*x*x

def phiprime(x):  #The derivative of the above function
    return 60*np.pi*x*x

def phidoubleprime(x):  #The derivative of the derivative of the two-above function
    return 120*np.pi*x

def f(x):  #u''(x) = f(x), which is this f
    return -20. + a*phidoubleprime(x)*np.cos(phi(x)) - a*(phiprime(x))**2 *np.sin(phi(x))

def true_solution(x):  #The analytic solution
    return 1 + 12*x - 10*x*x + a*np.sin(phi(x))

n_grid = 255 #Number of interior grid points
h = 1./float(n_grid+1) #size of grid spacing
u_0 = 1. #Left boundary condition
u_1 = 3. #Right boundary condition
epsilon = 1e-3  #The error tolerance I want to be within the true value before saying I've converged

iter_max = 50000 #Set a maximum number of iterations before quitting

#In what follows, we are trying to solve the matrix equation A u = b

A_diagonal = -2.  #The diagonol elements of my A matrix
A_offdiagonal = 1. #The off-diagonol elements of my tridiagonol A matrix

u = np.zeros(n_grid) #the solution vector

x_binedges = [] #This list will store the x-values of the edges of the cells
for i in range(n_grid):
    x_binedges.append(float(i+1)/float(n_grid+1))

#initialize the solution vector:
for i in range(n_grid):
    u[i] = initial_guess(x_binedges[i])

b = np.zeros(n_grid)  #The RHS vector

for i in range(n_grid): #Initialize this
    b[i] = f(x_binedges[i])*h*h

b[0] = b[0] - u_0 #Add LHS boundary condition to b
b[n_grid-1] = b[n_grid-1] - u_1 #Add RHS boundary condition to b

true_sol = np.zeros(n_grid) #Make a vector to store the values of the true solution

for i in range(n_grid): #Calculate values of true solution
    true_sol[i] = true_solution(x_binedges[i])

#Next two lines is to plot the true solution
x_toplot = np.linspace(0,1,200)
pp.plot(x_toplot,true_solution(x_toplot),label="True Solution",lw=4)

#Begin the iteration
for k in range(iter_max):
    for i in range(len(u)):

        if i==0:  #If we're on the first row, only add A[i][i+1]
            summation = A_offdiagonal * u[i+1]
        elif i== (len(u)-1): #If we're on the last row, only add A[i][i-1]
            summation = A_offdiagonal * u[i-1]
        else:  #Only need to add two elements together
            summation = A_offdiagonal * u[i+1] + A_offdiagonal * u[i-1]
        u[i] = 1./A_diagonal * (b[i] - summation) #Update u based on Jacobi algorithm

    #check for convergence
    numconverge = 0 #count the number of elements of u which have converged
    for i in range(len(u)):
        if abs(u[i] - true_sol[i]) < epsilon: #If converged within epsilon
                numconverge += 1

    if numconverge == len(u):  #If all the elements of x have converged
        print "Converged!  After %d loops" % (k+1)
        break

    #Plot the 20th, 100th, and 1000th iteration
    if k==(20-1) or k==(100-1) or k==(1000-1):
        s = str(k+1) + " iterations"
        pp.plot(x_binedges,u,label=s,lw=2.5)

else: #If for loop completes with convergence not being acheived
    print "Convergence not achieved after %d number of iterations" % (k+1)

#Plot the final solution and save te figure
pp.plot(x_binedges,u,label='final iteration', lw=2.5)
pp.xlabel("x")
pp.ylabel("u(x)")
pp.legend(loc='best')
pp.xlim(0,1)
pp.savefig('problem1a.pdf')


import numpy as np

'''
Problem 1.2
'''

def expo_func(omega, t):
    return np.exp(-1*omega*(t + np.abs(t)))

def first_forward(t, wdt, omega=1.):
    # For convenience, set omega = 1
    dt = wdt / omega
    # Evaluate
    tdt = t + dt
    expf = expo_func(omega, t)
    expf_plus = expo_func(omega, tdt)
    # Finite difference, forward
    dexpdt_forward = (expf_plus-expf)/dt
    return dexpdt_forward

def first_backward(t, wdt, omega=1.):
    # For convenience, set omega = 1
    dt = wdt / omega
    # Evaluate
    tdt = t - dt
    expf = expo_func(omega, t)
    expf_neg = expo_func(omega, tdt)
    # Finite difference, forward
    dexpdt_back = (expf-expf_neg)/dt
    return dexpdt_back

def second_centered(t, wdt, omega=1.):
    # For convenience, set omega = 1
    dt = wdt / omega
    # Evaluate
    tdtp = t + dt
    tdtn = t - dt
    expf_pos = expo_func(omega, tdtp)
    expf_neg = expo_func(omega, tdtn)
    # Finite difference, forward
    dexpdt_second = (expf_pos-expf_neg)/(2*dt)
    return dexpdt_second

'''
Problem 1.2
'''

def sinh_func(k, x):
    return np.sinh(k*x)

def sinh_first_forward(x, kdx, k=1.):
    dx = kdx / k
    # Evaluate
    xdx = x + dx
    expf = sinh_func(k, x)
    expf_plus = sinh_func(k, xdx)
    # Finite difference, forward
    dexpdt_forward = (expf_plus-expf)/dx
    return dexpdt_forward

def sinh_first_backward(x, kdx,k=1.):
    dx = kdx / k
    # Evaluate
    xdx = x - dx
    expf = sinh_func(k, x)
    expf_neg = sinh_func(k, xdx)
    # Finite difference, forward
    dexpdt_forward = (expf-expf_neg)/dx
    return dexpdt_forward

def sinh_second_center(x, kdx,k=1.):
    dx = kdx / k
    # Evaluate
    xdxn = x - dx
    xdxp = x + dx
    sinh_pos = sinh_func(k, xdxp)
    sinh_neg = sinh_func(k, xdxn)
    # Finite difference, forward
    dfdx_second = (sinh_pos-sinh_neg)/(2*dx)
    return dfdx_second

def sinh_fourth(x, kdx,k=1.):
    dx = kdx / k
    # Evaluate
    xdxn = x - dx
    xdxp = x + dx
    xdxn2 = x - 2*dx
    xdxp2 = x + 2*dx
    sinh_pos = sinh_func(k, xdxp)
    sinh_neg = sinh_func(k, xdxn)
    sinh_pos2 = sinh_func(k, xdxp2)
    sinh_neg2 = sinh_func(k, xdxn2)
    # Finite difference, forward
    dfdx_fourth = (4./3) * (sinh_pos-sinh_neg)/2/dx - (1/3)*(sinh_pos2-sinh_neg2)/4/dx
    return dfdx_fourth

'''
Problem 1.8
'''

def sin_func(omega, t, A=1., sigma=1e-5):
    # Noise
    noise = 2*np.random.random(np.atleast_1d(t).size) - 1
    Ap = A * (1 + noise * sigma)
    if not isinstance(t, np.ndarray):
        Ap = Ap[0]
    return Ap*np.sin(omega*t)

def sin_first_forward(t, omegadt, omega=1., sigma=1e-5):
    dt = omegadt / omega
    # Evaluate
    tdt = t + dt
    f = sin_func(omega, t, sigma=sigma)
    f_plus = sin_func(omega, tdt, sigma=sigma)
    # Finite difference, forward
    dfdt_forward = (f_plus-f)/dt
    return dfdt_forward

def sin_first_backward(t, omegadt,omega=1., sigma=1e-5):
    dt = omegadt / omega
    # Evaluate
    tdt = t - dt
    f = sin_func(omega, t, sigma=sigma)
    f_neg = sin_func(omega, tdt, sigma=sigma)
    # Finite difference, forward
    dfdt_back = (f-f_neg)/dt
    return dfdt_back

def sin_second_center(t, omegadt,omega=1., sigma=1e-5):
    dt = omegadt / omega
    tdtp = t + dt
    tdtn = t - dt
    f_neg = sin_func(omega, tdtn, sigma=sigma)
    f_pos = sin_func(omega, tdtp, sigma=sigma)
    dfdt_second = (f_pos-f_neg)/dt/2
    return dfdt_second


# Collection of simple optimization algorithms from the Computational Economics course
#
# by Fedor Iskhakov, 2020

import numpy as np

def bisection(f,a=0,b=1,tol=1e-6,maxiter=100,callback=None):
    '''Bisection method for solving equation f(x)=0
    on the interval [a,b], with given tolerance and number of iterations.
    Callback function is invoked at each iteration if given.
    '''
    if f(a)*f(b)>0:
        raise ValueError('Function has the same sign at the bounds')
    for i in range(maxiter):
        err = abs(b-a)
        if err<tol: break
        x = (a+b)/2
        a,b = (x,b) if f(a)*f(x)>0 else (a,x)
        if callback != None: callback(err=err,x=x,iter=i)
    else:
        raise RuntimeError('Failed to converge in %d iterations'%maxiter)
    return x

def newton(fun,grad,x0,tol=1e-6,maxiter=100,callback=None):
    '''Newton method for solving equation f(x)=0
    with given tolerance and number of iterations.
    Callback function is invoked at each iteration if given.
    '''
    for i in range(maxiter):
        x1 = x0 - fun(x0)/grad(x0)
        err = abs(x1-x0)
        if callback != None: callback(err=err,x0=x0,x1=x1,iter=i)
        if err<tol: break
        x0 = x1
    else:
        raise RuntimeError('Failed to converge in %d iterations'%maxiter)
    return (x0+x1)/2

def solve_sa(F,x0,tol=1e-6,maxiter=100,callback=None):
    '''Computes the solution of fixed point equation x = F(x)
    with given initial value x0 and algorithm parameters
    Method: successive approximations
    '''
    for i in range(maxiter):  # main loop
        x1 = F(x0)  # update approximation
        err = np.amax(abs(x0-x1))  # allow for x to be array
        if callback != None: callback(iter=i,err=err,x=x1,x0=x0)
        if err<tol:
            break  # break out if converged
        x0 = x1  # get ready to the next iteration
    else:
        raise RuntimeError('Failed to converge in %d iterations'%maxiter)
    return x1

def newton2(fun,grad,x0,tol=1e-6,maxiter=100,callback=None):
    '''Newton method for solving equation f(x)=0, x is vector of 2 elements,
    with given tolerance and number of iterations.
    Callback function is invoked at each iteration if given.
    '''
    # conversion to array function of array argument
    npfun = lambda x: np.asarray(fun(x[0],x[1]))
    npgrad = lambda x: np.asarray(grad(x[0],x[1]))
    for i in range(maxiter):
        x1 = x0 - np.linalg.inv(npgrad(x0)) @ npfun(x0)  # matrix version
        err = np.amax(np.abs(x1-x0))  # vector version
        if callback != None: callback(iter=i,err=err,x0=x0,x1=x1,fun=fun)
        if err<tol: break
        x0 = x1
    else:
        raise RuntimeError('Failed to converge in %d iterations'%maxiter)
    return (x0+x1)/2

# def grid_search(fun,bounds=(0,1),ngrid=10):
#     '''Grid search between given bounds over given number of points'''
#     grid = np.linspace(*bounds,ngrid)
#     func = fun(grid)
#     i = np.argmax(func)  # index of the element attaining maximum
#     return grid[i]


# Project Euler Problem 9 Solution
#
# Problem statement:
# A Pythagorean triplet is a set of three natural numbers,
# a < b < c, for which, a^2 + b^2 = c^2. For example,
# 3^2 + 4^2 = 9 + 16 = 25 = 5^2. There exists exactly one
# Pythagorean triplet for which a + b + c = 1000. Find the
# product abc.
#
# Solution description:
# --
#
# Author: Daniel Schuette
# Date: 2018/02/12
# License: MIT (see ../LICENSE.md)
import time

import numpy as np

def pythagorean_triplet(a, b, c):
    """
    Tests whether a^2 + b^2 = c^2.
    """
    return a**2 + b**2 == c**2

def find_triplet(n):
    """
    Finds a Pythagorean triplet for which
    a + b + c = n and returns the product a*b*c.
    """
    i, j, k = 0, 0, 0
    for i in range(1, n):
        for j in range(1, n):
            k = np.sqrt(i**2 + j**2)
            if pythagorean_triplet(i, j, k) and (i+j+k) == n:
                return i*j*k

if __name__ == "__main__":
    # calculate result
    start = time.time()
    solution = find_triplet(1000)
    end = time.time()

    # print result
    print(f"Solution: {solution}")
    print(f"Elapsed time: {(end - start):.5f}s")


import numpy as np
from numpy.linalg import norm

from random import normalvariate
from math import sqrt

def random_unit_vector(n):
    unnormalized = [normalvariate(0, 1) for _ in range(n)]
    the_norm = sqrt(sum(x * x for x in unnormalized))
    return [x / the_norm for x in unnormalized]

def svd_1d(A, epsilon=1e-10):
    '''Compute the one-dimensional SVD.

    Arguments:
        A: an n-by-m matrix
        epsilon: a tolerance

    Returns:
        the top singular vector of A.
    '''

    n, m = A.shape
    x = random_unit_vector(min(n, m))
    last_v = None
    current_v = x

    if n > m:
        B = np.dot(A.T, A)
    else:
        B = np.dot(A, A.T)

    iterations = 0
    while True:
        iterations += 1
        last_v = current_v
        current_v = np.dot(B, last_v)
        current_v = current_v / norm(current_v)

        if abs(np.dot(current_v, last_v)) > 1 - epsilon:
            print("converged in {} iterations!".format(iterations))
            return current_v

def svd(A, k=None, epsilon=1e-10):
    '''Compute the singular value decomposition of a matrix A using
    the power method.

    Arguments:
        A: an n-by-m matrix
        k: the number of singular values to compute
           If k is None, compute the full-rank decomposition.
        epsilon: a tolerance factor

    Returns:
        A tuple (S, u, v), where S is a list of singular values,
        u is an n-by-k matrix containing the left singular vectors,
        v is a k-by-m matrix containnig the right-singular-vectors
    '''
    A = np.array(A, dtype=float)
    n, m = A.shape
    svd_so_far = []
    if k is None:
        k = min(n, m)

    for i in range(k):
        matrix_for_1d = A.copy()

        for singular_value, u, v in svd_so_far[:i]:
            matrix_for_1d -= singular_value * np.outer(u, v)

        if n > m:
            v = svd_1d(matrix_for_1d, epsilon=epsilon)  # next singular vector
            u_unnormalized = np.dot(A, v)
            sigma = norm(u_unnormalized)  # next singular value
            u = u_unnormalized / sigma
        else:
            u = svd_1d(matrix_for_1d, epsilon=epsilon)  # next singular vector
            v_unnormalized = np.dot(A.T, u)
            sigma = norm(v_unnormalized)  # next singular value
            v = v_unnormalized / sigma

        svd_so_far.append((sigma, u, v))

    singular_values, us, vs = [np.array(x) for x in zip(*svd_so_far)]
    return singular_values, us.T, vs


import numpy as np
from numpy import linalg as la
from typing import Union

def mahalanobis_dist(data, x):
    """Compute mahalanobis distance of point x to data."""
    C = np.cov(data.T)
    mu = np.mean(data, axis=0)
    IC = la.inv(C)
    dx = x - mu
    return np.sqrt(dx.T@IC@dx)

def mahalanobis_cont(d1, d2):
    """Compute mahalanobis distance for continuous variables."""
    mu1 = np.mean(d1, axis=0)
    mu2 = np.mean(d2, axis=0)
    C = np.cov(np.concatenate([d1, d2]).T)
    IC = la.inv(C)
    dmu = mu1-mu2
    return np.sqrt(dmu.T@IC@dmu)

def mahalanobis_bin(d1, d2):
    """Compute mahalanobis distance for binary variables.
    If for one of the groups the estimated probabilitiy is zero
        result is the absolute difference between probabilities,
    if both est. probs are zero result is zero,
    else: result is given by (p1-p2)*np.log(p1/p2)
    in the last step we sum over variables.
        """
    d1 = d1.to_numpy()
    d2 = d2.to_numpy()
    p1 = np.sum(d1, axis=0)/len(d1)
    p2 = np.sum(d2, axis=0)/len(d2)
    logarg = np.divide(p1, p2, out=np.ones_like(p1), where=((p1!=0) & (p2!=0)))
    result = (p1-p2)*np.log(logarg,out=np.zeros_like(p1),
                            where=((p1!=0) & (p2!=0)))
    zero_one_mask = np.logical_xor(p1==0, p2==0)
    result[zero_one_mask] = np.abs(p1[zero_one_mask]-p2[zero_one_mask])
    return np.sum(result)

def gen_mahalanobis(df, g1:Union[None, list]=None, g2:Union[None, list]=None)->float:
    """Takes dataframe and computes generalised mahalanobis distance between the two groups.
    Given in the paper:
        Barhen, Avner, and J. J. Daudin.
        "Generalization of the Mahalanobis distance in the mixed case."
        Journal of Multivariate Analysis 53.2 (1995): 332-342.
    By default the two groups are exposed==1 and exposed==0
    Optinally one can pass two lists of indices g1 and g2.
    """
    bin_var_cols = [k for k in df.keys() if k.startswith('b')]
    cont_var_cols = [k for k in df.keys() if k.startswith('x')]
    if isinstance(g1, type(None)) or isinstance(g2, type(None)):
        df1_c = df[cont_var_cols][df.exposed==0]
        df2_c = df[cont_var_cols][df.exposed==1]
        df1_b = df[bin_var_cols][df.exposed==0]
        df2_b = df[bin_var_cols][df.exposed==1]
    else:
        df1_c = df.loc[g1][cont_var_cols]
        df2_c = df.loc[g2][cont_var_cols]
        df1_b = df.loc[g1][bin_var_cols]
        df2_b = df.loc[g2][bin_var_cols]
    Jc = mahalanobis_cont(df1_c, df2_c)
    Jb = mahalanobis_bin(df1_b, df2_b)
    return Jb + Jc


# Calculating the Value of Pi using Random Numbers (0,1)
# Joao Pinheiro
# Import packages
import random
import numpy as np
import matplotlib.pyplot as plt

# User input
n = input('Insert the number of random points: ')
print('The number of random points is ' + n)

# The function to calculate pi
def calc_pi(n):
    NumPointsCircle = 0
    NumTotalPoints  = 0
    for _ in range(n):
        x = random.uniform(0,1)
        y = random.uniform(0,1)
        Distance = np.sqrt( x**2 + y**2 ) # Euclidian distance
        if Distance <= 1:
            NumPointsCircle += 1
        NumTotalPoints += 1
    # pi / 4 = Number of Points in Circule / Number of Total Points
    pi = 4 * ( NumPointsCircle / NumTotalPoints )

    return pi

print( 'The value of pi is: ' + str(calc_pi( int(n) )) )


from __future__ import annotations
from typing import Union
import numpy as np

class PCA:
    """PCA - Principal Component Analysis
    Parameters:
    -----------
    n_components: int = 2
        Number of components to keep.
    """
    def __init__(self, n_components: int = 2):
        self.n_components = n_components
        self.eigenvalues = None
        self.eigenvectors = None
        self.mean = None
        self.std = None

    def fit(self, X: Union[list, np.ndarray]) -> PCA:
        # subtract off the mean to center the data
        self.mean = X.mean(axis=0)
        X = X - self.mean

        covariance_matrix = np.cov(X, rowvar=False)

        eigenvalues, eigenvectors = np.linalg.eig(covariance_matrix)

        idx = eigenvalues.argsort()[::-1]
        eigenvalues = eigenvalues[idx][:self.n_components]
        eigenvectors = np.atleast_1d(eigenvectors[:, idx])[:, :self.n_components]

        self.eigenvalues = eigenvalues
        self.eigenvectors = eigenvectors

        return self

    def fit_transform(self, X: Union[list, np.ndarray]) -> np.ndarray:
        self.fit(X)
        return self.transform(X)

    def transform(self, X: Union[list, np.ndarray]) -> np.ndarray:
        X = X - self.mean

        # Project the data onto principal components
        X_transformed = np.dot(X, self.eigenvectors)

        return X_transformed

    @property
    def explained_variance_ratio_(self):
        return self.eigenvalues / np.sum(self.eigenvalues)

if __name__ == '__main__':
    # source: https://scikit-learn.org/stable/auto_examples/decomposition/plot_pca_iris.html
    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d import Axes3D
    from sklearn import datasets

    np.random.seed(5)

    centers = [[1, 1], [-1, -1], [1, -1]]
    iris = datasets.load_iris()
    X = iris.data
    y = iris.target

    fig = plt.figure(1, figsize=(4, 3))
    plt.clf()
    ax = Axes3D(fig, rect=[0, 0, .95, 1], elev=48, azim=134)

    plt.cla()
    pca = PCA(n_components=3)
    pca.fit(X)
    X = pca.transform(X)

    for name, label in [('Setosa', 0), ('Versicolour', 1), ('Virginica', 2)]:
        ax.text3D(X[y == label, 0].mean(),
                X[y == label, 1].mean() + 1.5,
                X[y == label, 2].mean(), name,
                horizontalalignment='center',
                bbox=dict(alpha=.5, edgecolor='w', facecolor='w'))
    # Reorder the labels to have colors matching the cluster results
    y = np.choose(y, [1, 2, 0]).astype(float)
    ax.scatter(X[:, 0], X[:, 1], X[:, 2], c=y, cmap=plt.cm.nipy_spectral,
            edgecolor='k')

    ax.w_xaxis.set_ticklabels([])
    ax.w_yaxis.set_ticklabels([])
    ax.w_zaxis.set_ticklabels([])

    plt.show()


import numpy as np

class Hamming:
    """Hamming encoding of strings: {1,0}*

    r = number of parity bits per block
    m = number of data bits per block = 2^r - r - 1
    n = number of total bits per block = 2^r - 1

    reference: http://www-math.mit.edu/~djk/18.310/18.310F04/matrix_hamming_codes.html
    """

    def __init__(self, r):
        self.r = r
        self.m = 2**r - r - 1
        self.n = 2**r - 1

        num_digits = len(np.binary_repr(self.n))

        # non systematic h
        h = np.matrix([[int(x) for x in np.binary_repr(i, num_digits)] for i in range(1, self.n+1)])
        h = h.T

        # convert last r cols to identity matrix through swapping columns (systematic)
        for i in range(r):
            x = 2**i - 1
            y = self.n - 1 - i
            h[:,[x,y]] = h[:,[y,x]]

        g = np.concatenate((np.identity(self.m, dtype=int), h[:,:self.m].T), axis=1)

        self.decoding_matrix = h
        self.encoding_matrix = g

    def encode(self, s):
        """Encodes a string of {1,0}* with length a multiple of m"""
        if len(s) % self.m != 0:
            print("Error: encode input string length must be multiple of m")
            return None
        s_array = np.array(list(s), dtype=int)
        s_array_split = np.split(s_array, indices_or_sections=len(s)/self.m)
        s_array_mult = [np.remainder(np.dot(x, self.encoding_matrix), 2) for x in s_array_split]
        out_s = np.concatenate(s_array_mult, axis=1)
        flattened = np.ravel(out_s)
        return "".join([str(x) for x in flattened.tolist()])

    def decode(self, s):
        """Encodes a string of {1,0}* with length a multiple of n"""
        if len(s) % self.n != 0:
            print("Error: decode input string length must be multiple of n")
            return None

        syndrome_mapping = {}
        d = self.decoding_matrix.T
        for i in range(len(d)):
            curr_num_s = "".join([str(x) for x in d[i].tolist()[0]])
            curr_num = int(curr_num_s, 2)
            syndrome_mapping[curr_num] = i

        s_array = np.array(list(s), dtype=int)
        s_array_split = np.split(s_array, indices_or_sections=len(s)/self.n)
        s_array_mult = [np.remainder(np.dot(self.decoding_matrix, x), 2) for x in s_array_split]

        out = ""
        for i in range(len(s_array_mult)):
            curr_num_s = "".join([str(x) for x in s_array_mult[i].tolist()[0]])
            curr_num = int(curr_num_s, 2)
            curr_out = s_array_split[i].tolist()
            if curr_num != 0:
                incorrect = syndrome_mapping[curr_num]
                curr_out[incorrect] = 1-curr_out[incorrect]
            out += "".join([str(x) for x in curr_out[:self.m]])
        return out


import numpy
numpy.random.seed(42)

print("Nonlinear equations \n")

EPS = 10.0 ** -5

def input():
    #expr = numpy.poly1d([1.0, 1.0, 0.0])
    #expr = numpy.poly1d(numpy.poly([1.0, 2.0, 3.0]))
    #expr = numpy.poly1d(numpy.poly([1.0, 1.0, 1.0]))
    #expr = numpy.poly1d(numpy.poly([0.0, 0.0, 1.0]))

    expr = numpy.poly1d([1.0, 38.4621, 364.594, 914.196])

    #expr = numpy.poly1d([1.0, -6.4951, -31.2543, 23.1782])

    return (expr)

(f) = input()
print(f)

def SturmSeq(f):
    arr = []
    arr.append(f)
    arr.append(numpy.polyder(f))

    while True:
        fn = -numpy.polydiv(arr[-2], arr[-1])[1]
        if (fn.order > 0 or abs(fn[0]) > 0.0):
            arr.append(fn)
        else:
            break

    return arr

def N(stseq, x):
    if (abs(f(x)) < EPS):
        raise ValueError("Number in N() is a root")
    ans = 0
    for i in range(1, len(stseq)):
        if (stseq[i](x) == 0.0):
            raise ValueError("SturmSeq[i] is zero")
        if(stseq[i-1](x) * stseq[i](x) < 0):
            ans += 1
    return ans

def GetBounds(f, a, b):
    if ((abs(f(a)) < EPS) or (abs(f(b)) < EPS)):
        raise ValueError("Bounds contain root")
    if (N(Sturm, a) - N(Sturm, b) == 0):
        return []
    if (N(Sturm, a) - N(Sturm, b) > 1):
        while True:
            M = a + (b - a) / (1.5 + numpy.random.random())
            if (abs(f(M)) > EPS):
                break
        return GetBounds(f, a, M) + GetBounds(f, M, b)
    if (b - a < EPS):
        print("Warning: Bounds are too small")
    return [(a, b)]

Sturm = SturmSeq(f)

print("Amount of roots on [-10, 10]:")
print(N(Sturm, -10) - N(Sturm, 10))

bounds = GetBounds(f, -10, 10)
print("Roots are in bounds:")
print(bounds)

iters = 0

def BinarySearch(L, R):
    global iters
    iters += 1
    M = (L + R) / 2
    if (R - L < EPS):
        return M
    #if (abs(f(M)) < EPS):
    #    return M
    if (f(L) * f(M) <= 0):
        return BinarySearch(L, M)
    elif (f(R) * f(M) <= 0):
        return BinarySearch(M, R)
    else:
        raise RuntimeError("Something went wrong in BinarySearch")

def SecantFirst(L, R):
    global iters
    fder2 = numpy.polyder(f, 2)
    if (f(R) * fder2(R) > 0):
        (oldx, x) = (R, L)
    elif (f(L) * fder2(L) > 0):
        (oldx, x) = (L, R)
    else:
        raise ValueError("Bad bounds in first Secant method")
    t = oldx
    while (abs(x - oldx) > EPS):
    #while (abs(f(x)) > EPS):
        iters += 1
        oldx = x
        x = x - f(x) * (t - x) / (f(t) - f(x))
        if (not (numpy.isfinite(x))):
            raise RuntimeError("Something went wrong, and x is not a number")
    if ((x < L) or (R < x)):
        raise RuntimeError("Something went wrong, and x is not in [L, R]")
    return x

def SecantSecond(L, R):
    global iters
    (x, oldx) = (L, R)
    while (abs(x - oldx) > EPS):
    #while (abs(f(x)) > EPS):
        iters += 1
        oldx = x
        x = L - f(L) * (R - L) / (f(R) - f(L))
        if (not (numpy.isfinite(x))):
            raise RuntimeError("Something went wrong, and x is not a number")
        if (f(L) * f(x) <= 0):
            R = x
        elif (f(R) * f(x) <= 0):
            L = x
        else:
            raise RuntimeError("Something went wrong in second Secant method")
    if ((x < L) or (R < x)):
        raise RuntimeError("Something went wrong, and x is not in [L, R]")
    return x

def Newton(L, R):
    global iters
    fder = numpy.polyder(f)
    fder2 = numpy.polyder(f, 2)
    if (f(L) * fder2(L) > 0):
        (oldx, x) = (R, L)
    elif (f(R) * fder2(R) > 0):
        (oldx, x) = (L, R)
    else:
        raise ValueError("Bad bounds in Newton method")
    while (abs(x - oldx) > EPS):
    #while (abs(f(x)) > EPS):
        iters += 1
        oldx = x
        x = x - f(x) / fder(x)
        if (not (numpy.isfinite(x))):
            raise RuntimeError("Something went wrong, and x is not a number")
    if ((x < L) or (R < x)):
        raise RuntimeError("Something went wrong, and x is not in [L, R]")
    return x

numpy.set_printoptions(suppress = True, precision = 4, floatmode = "fixed")

def test(method):
    global iters
    for i in range(len(bounds)):
        iters = 0
        try:
            str = method(*bounds[i])
            if  (not str is None):
                str = "{:.4f}".format(str)
            print("{} via {} method (with {} iterations)".format(str, method.__name__, iters))
        except Exception as ex:
            print("ERROR: {} - in {} method (with {} iterations)".format(ex, method.__name__, iters))

print()
test(BinarySearch)
print()
test(SecantFirst)
print()
test(SecantSecond)
print()
test(Newton)
print()

print("Check:")
print(f.r)


# -*- coding: utf-8 -*-
import numpy as np
import math,cmath

def computecorrelation(x,y):
    x_bar=np.mean(x)
    y_bar=np.mean(y)
    SSR=0
    Varx=0
    Vary=0
    for i in range(0,len(x)):
        SSR+=(x[i]-x_bar)*(y[i]-y_bar)
        Varx+=(x[i]-x_bar)**2
        Vary+=(y[i]-y_bar)**2
    SST=cmath.sqrt(Varx*Vary)
    return SSR/SST

def belog(x):
    logx = []
    for i in x:
        if(i > 0):
            logx.append(math.log(i,10))
        elif(i < 0):
            logx.append(0)
    return logx

def polyfot(x,y,degree):
    result={}
    coef=np.polyfit(x,y,degree)#算出各个回归系数
    result["polynomial"]=coef.tolist()
    p=np.poly1d(coef)#拟合一条线
    y_hat=p(x)
    y_bar=np.mean(y)
    SSR=np.sum((y_hat-y_bar)**2)
    SST=np.sum((y-y_bar)**2)
    result["determination"]=SSR/SST
    return result["determination"]
def rmse(x,y):
    z = []
    for i in range(len(x)):
        z.append((x[i] - y[i])**2)
    z_bar = np.mean(z)
    return np.sqrt(z_bar)
def mae(x,y):
    z = []
    for i in range(len(x)):
        z.append(abs(x[i] - y[i]))
    return np.mean(z)
def get_stastics(y1,y2):
    mae1 = mae(y1,y2)
    rmse1 = rmse(y1,y2)
    cor = computecorrelation(y1,y2)
    print('The MAE is: ',mae1)
    print('The RMSE is: ',rmse1)
    print('The correlation parameter is: ',cor)
    return
def vdot(v1,v2):
    v3 = 0
    if(len(v1) == len(v2)):
        for i in range(len(v1)):
            v3 += v1[i] * v2[i]
    return v3

def vcross(v1,v2):
    v3 = [0,0,0]
    if(len(v1) == len(v2)):
        v3[0] = v1[1] * v2[2] - v1[2] * v2[1]
        v3[1] = v1[2] * v2[0] - v1[0] * v2[2]
        v3[2] = v1[0] * v2[1] - v1[1] * v2[0]
        return v3
    return v3

def zscore(mean,std,av):
    import numpy as np
    import math
    stdav = []
    for i in range(len(mean)):
        if std[i] < 1e-15:
            temp = 0
        else:
            temp = (av[i] - mean[i])/(math.sqrt(std[i]))
        stdav.append(temp)
    return np.array(stdav)

def list_split(lists,clist):
    l1 = []
    l2 = []
    nl = [ i for i in range(0,len(lists))]
    for i in clist:
        l1.append(lists[i])
    for i in nl:
        if i not in clist:
            l2.append(lists[i])
    l1 = np.array(l1)
    l2 = np.array(l2)
    return l1,l2

def train_test_split(x_data,y_data,train_size=0.8,test_size=0.2, random_state=0):
    import random
    random.seed(random_state)
    nx = len(x_data)
    ny = len(y_data)
    x_train = []
    x_test = []
    y_train = []
    y_test = []
    if nx == ny:
        if train_size <= 1 and test_size <= 1:
            n1 = int(nx * train_size)
            n2 = int(nx * test_size)
            nlist = [i for i in range(0,nx)]
            clist = random.sample(nlist,n1)
            x_train, x_test = list_split(x_data,clist)
            y_train, y_test = list_split(y_data,clist)
            return x_train,x_test,y_train,y_test
        else:
            nlist = [i for i in range(0,nx)]
            clist = random.sample(nlist,train_size)
            x_train, x_test = list_split(x_data,clist)
            y_train, y_test = list_split(y_data,clist)
            return x_train,x_test,y_train,y_test
    else:
        return 0

if __name__ == '__main__':
    v1 = [1,2,3]
    v2 = [4,5,6]
    v3 = vcross(v1,v2)
    print(vdot(v1,v2))
    print(v3)

    x_data = [[1,2],[3,4],[5,6],[7,8],[9,10]]
    y_data = [1,2,3,4,5]
    x1,x2,y1,y2 = train_test_split(x_data,y_data,train_size=0.8,test_size=0.2,random_state=23)
    print(x1)
    print(y1)


import numpy as np
# 矩阵乘法的几何意义 https://zhuanlan.zhihu.com/p/64606322
# 矩阵乘法  矩阵积 点积  matrix product
a = np.array([[4, 5], [6, 7]])
b = np.arange(4).reshape((2, 2))
print(a)
# [[4 5]
#  [6 7]]
print(b)
# [[0 1]
#  [2 3]]
# 矩阵乘法
c_dot = np.dot(a,b)
print(c_dot)
# [[10 19]
#  [14 27]]
c_dot_2 = a.dot(b)
# [[10 19]
#  [14 27]]
print(a @ b)

print(a * b)
"""
a1,1 * b1,1 + a1,2 * b2,1 + a1,n * bn,1  ,  a1,1 * b1,2 + a1,2 * b2,2 + a1,n * bn,2
a2,1 * b1,1 + a2,2 * b2,1 + a2,n * bn,1  ,  a2,1 * b1,2 + a2,2 * b2,2 + a2,n * bn,2
= A 的行数 ，B的列数
= [
[4*0+5*2,4*1+5*3],
[6*0+7*2,6*1+7*3]
]
"""

a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
print(np.inner(a, b)) # 内积 Inner product
print(np.dot(a, b)) # 点积 Dot product
print(np.cross(a, b)) # 叉积  cross product
print(np.outer(a, b)) # 外积 outer product
print(a*b)
print(a@b) # 等于 np.dot(a, b)

a = np.array([3, 1])
b = np.array([2, -1])
print(np.inner(a, b)) # 内积 Inner product
print(np.dot(a, b))    # 点积 Dot product
print(np.cross(a, b)) # 叉积  cross product
print(np.outer(a, b)) # 外积 outer product
print(a*b)
print(a@b)
# 向量的点积=内积

print(np.linalg.det([[3,2],[1,-1]])) # 行列式 = 叉积


# Chapter 4: Granger Causality Test

In the first three chapters, we discussed the classical methods for both univariate and multivariate time series forecasting. We now introduce the notion of causality and its implications on time series analysis in general. We also describe a test for the linear VAR model discussed in the previous chapter.

Prepared by: Carlo Vincienzo G. Dajac

## Notations

If $A_t$ is a stationary stochastic process, let $\overline A_t$ represent the set of *past* values ${A_{t-j}, \; j=1,2,\ldots,\infty}$ and $\overline{\overline A}_t$ represent the set of *past and present* values ${A_{t-j}, \; j=0,1,\ldots,\infty}$. Further, let $\overline A(k)$ represent the set ${A_{t-j}, \; j=k,k+1,\ldots,\infty}$.

Denote the optimum, unbiased, least-squares predictor of $A_t$ using the set of values $B_t$ by $P_t (A|B)$. Thus, for instance, $P_t (X|\overline X)$ will be the optimum predictor of $X_t$ using only past $X_t$. The predictive error series will be denoted by $\varepsilon_t(A|B) = A_t - P_t(A|B)$. Let $\sigma^2 (A|B)$ be the variance of $\varepsilon_t(A|B)$.

Let $U_t$ be all the information in the universe accumulated since time $t-1$ and let $U_t - Y_t$ denote all this information *apart* from the specified series $Y_t$, which is another stationary time series that is different from $X_t$.

## Definitions

### Causality

If $\sigma^2 (X|U) < \sigma^2 (X| \overline{U-Y})$, we say that $Y$ is causing $X$, denoted by $Y_t \implies X_t$. We say that $Y_t$ is causing $X_t$ if we are **able to predict** $X_t$ using all available information than if the information apart from $Y_t$ had been used.

### Feedback
If $\sigma^2 (X|\overline U) < \sigma^2 (X| \overline{U-Y})$ and $\sigma^2 (Y|\overline U) < \sigma^2 (Y| \overline{U-X})$, we say that feedback is occurring, which is denoted by $Y_t \iff X_t$, i.e., feedback is said to occur when $X_t$ is causing $Y_t$ and also $Y_t$ is causing $X_t$.

### Instantaneous Causality
If $\sigma^2 (X|\overline U, \overline{\overline Y}) < \sigma^2 (X| \overline U)$, we say that instantaneous causality $Y_t \implies X_t$ is occurring. In other words, the current value of $X_t$ is better "predicted" if the present value of $Y_t$ is included in the "prediction" than if it is not.

### Causality Lag
If $Y_t \implies X_t$, we define the (integer) causality lag $m$ to be the least value of $k$ such that $\sigma^2 (X|U-Y(k)) < \sigma^2 (X|U-Y(k+1))$. Thus, knowing the values $Y_{t-j}, \; j=0,1,\ldots,m-1$ will be of no help in improving the prediction of $X_t$

## Assumptions

* $X_t$ and $Y_t$ are stationary.
* $P_t (A|B)$ is already optimized.

## Testing for Granger Causality

We will be first building VAR models for our examples in this section. In addition to the steps outlined in the previous chapter, we will just call built-in Granger causality test function and configure it accordingly.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from pandas.plotting import lag_plot
from statsmodels.tsa.vector_ar.var_model import VAR
from statsmodels.tsa.stattools import adfuller, kpss, grangercausalitytests
import warnings
warnings.filterwarnings("ignore")

### Example 1: Ipo Dam Dataset

We will use the Ipo dataset in this example. It contains daily measurements of the following variables: rainfall (in millimeters), Oceanic Niño Index (ONI), NIA release flow (in cubic meters per second), and dam water level (in meters), respectively.

ipo_df = pd.read_csv('../data/Ipo_dataset.csv', index_col='Time');
ipo_df = ipo_df.dropna()
ipo_df.head()

fig,ax = plt.subplots(4, figsize=(15,8), sharex=True)
plot_cols = ['Rain', 'ONI', 'NIA', 'Dam']
ipo_df[plot_cols].plot(subplots=True, legend=False, ax=ax)
for a in range(len(ax)):
    ax[a].set_ylabel(plot_cols[a])
ax[-1].set_xlabel('')
plt.tight_layout()
plt.show()

#### Causality between Rainfall and Ipo Dam Water Level

For this example, we will focus on the Rain and Dam time series.

data_df = ipo_df.drop(['ONI', 'NIA'], axis=1)
data_df.head()

We look at the lag plots to quickly check for stationarity.

def lag_plots(data_df):
    f, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))

    lag_plot(data_df[data_df.columns[0]], ax=ax1)
    ax1.set_title(data_df.columns[0]);

    lag_plot(data_df[data_df.columns[1]], ax=ax2)
    ax2.set_title(data_df.columns[1]);

    ax1.set_ylabel('$y_{t+1}$');
    ax1.set_xlabel('$y_t$');
    ax2.set_ylabel('$y_{t+1}$');
    ax2.set_xlabel('$y_t$');

    plt.tight_layout()

lag_plots(data_df)

**Result:** Dam does not look stationary. Rainfall lag plot is inconclusive.

We use KPSS and ADF tests discussed in the previous chapter to conclusively check for stationarity.

def kpss_test(data_df):
    test_stat, p_val = [], []
    cv_1pct, cv_2p5pct, cv_5pct, cv_10pct = [], [], [], []
    for c in data_df.columns:
        kpss_res = kpss(data_df[c].dropna(), regression='ct')
        test_stat.append(kpss_res[0])
        p_val.append(kpss_res[1])
        cv_1pct.append(kpss_res[3]['1%'])
        cv_2p5pct.append(kpss_res[3]['2.5%'])
        cv_5pct.append(kpss_res[3]['5%'])
        cv_10pct.append(kpss_res[3]['10%'])
    kpss_res_df = pd.DataFrame({'Test statistic': test_stat,
                               'p-value': p_val,
                               'Critical value - 1%': cv_1pct,
                               'Critical value - 2.5%': cv_2p5pct,
                               'Critical value - 5%': cv_5pct,
                               'Critical value - 10%': cv_10pct},
                             index=data_df.columns).T
    kpss_res_df = kpss_res_df.round(4)
    return kpss_res_df

kpss_test(data_df)

**Result:** Rain is stationary, while Dam is not.

def adf_test(data_df):
    test_stat, p_val = [], []
    cv_1pct, cv_5pct, cv_10pct = [], [], []
    for c in data_df.columns:
        adf_res = adfuller(data_df[c].dropna())
        test_stat.append(adf_res[0])
        p_val.append(adf_res[1])
        cv_1pct.append(adf_res[4]['1%'])
        cv_5pct.append(adf_res[4]['5%'])
        cv_10pct.append(adf_res[4]['10%'])
    adf_res_df = pd.DataFrame({'Test statistic': test_stat,
                               'p-value': p_val,
                               'Critical value - 1%': cv_1pct,
                               'Critical value - 5%': cv_5pct,
                               'Critical value - 10%': cv_10pct},
                             index=data_df.columns).T
    adf_res_df = adf_res_df.round(4)
    return adf_res_df

adf_test(data_df)

**Result:** Both data are stationary.

Since both the lag plot and KPSS test indicate that Dam is not stationary, we apply differencing first before building our VAR model.

data_df['Dam'] = data_df['Dam'] - data_df['Dam'].shift(1)
data_df = data_df.dropna()

We again look at the lag plots and apply the KPSS and ADF tests.

lag_plots(data_df)

kpss_test(data_df)

adf_test(data_df)

**Result:** All three conclusively agree that both data are now stationary.

We next split the data into train and test sets for the VAR model.

def splitter(data_df):
    end = round(len(data_df)*.8)
    train_df = data_df[:end]
    test_df = data_df[end:]
    return train_df, test_df

train_df, test_df = splitter(data_df)

We then select the VAR order $p$ by computing the different multivariate information criteria (AIC, BIC, HQIC), and FPE.

def select_p(train_df):
    aic, bic, fpe, hqic = [], [], [], []
    model = VAR(train_df)
    p = np.arange(1,60)
    for i in p:
        result = model.fit(i)
        aic.append(result.aic)
        bic.append(result.bic)
        fpe.append(result.fpe)
        hqic.append(result.hqic)
    lags_metrics_df = pd.DataFrame({'AIC': aic,
                                    'BIC': bic,
                                    'HQIC': hqic,
                                    'FPE': fpe},
                                   index=p)
    fig, ax = plt.subplots(1, 4, figsize=(15, 3), sharex=True)
    lags_metrics_df.plot(subplots=True, ax=ax, marker='o')
    plt.tight_layout()
    print(lags_metrics_df.idxmin(axis=0))

select_p(train_df)

**Result:** We see that BIC has the lowest value at $p=8$ while HQIC  at $p=11$. Although both AIC and FPE have the lowest value at $p=21$, their plots also show an elbow. We can thus select the number of lags to be 8 (also for computational efficiency).

We now fit the VAR model with the chosen order.

p = 8
model = VAR(train_df)
var_model = model.fit(p)

We can finally test the variables for Granger Causality

def granger_causation_matrix(data, variables, p, test = 'ssr_chi2test', verbose=False):
    """Check Granger Causality of all possible combinations of the time series.
    The rows are the response variables, columns are predictors. The values in the table
    are the P-Values. P-Values lesser than the significance level (0.05), implies
    the Null Hypothesis that the coefficients of the corresponding past values is
    zero, that is, the X does not cause Y can be rejected.

    data      : pandas dataframe containing the time series variables
    variables : list containing names of the time series variables.
    """
    df = pd.DataFrame(np.zeros((len(variables), len(variables))), columns=variables, index=variables)
    for c in df.columns:
        for r in df.index:
            test_result = grangercausalitytests(data[[r, c]], p, verbose=False)
            p_values = [round(test_result[i+1][0][test][1],4) for i in range(p)]
            if verbose: print(f'Y = {r}, X = {c}, P Values = {p_values}')
            min_p_value = np.min(p_values)
            df.loc[r, c] = min_p_value
    df.columns = [var + '_x' for var in variables]
    df.index = [var + '_y' for var in variables]
    return df

granger_causation_matrix(train_df, train_df.columns, p)

**Recall:** If a given p-value is < significance level (0.05), then, the corresponding X series (column) causes the Y (row).

**Results:** For this particular example, we can say that rainfall Granger causes changes in the dam water level. This means that rainfall data improves changes in dam water level prediction performance.

On the other hand, changes in dam water level does not Granger cause rainfall. This means that changes in dam water level data does not improve rainfall prediction performance.

#### Causality between NIA Release Flow and Ipo Dam Water Level

In this next example, we now focus on the NIA and Dam time series.

data_df = ipo_df.drop(['ONI', 'Rain'], axis=1)
data_df.head()

We first check for stationarity by looking at the lag plots and applying the KPSS and ADF tests.

lag_plots(data_df)

kpss_test(data_df)

adf_test(data_df)

**Result:** All three conclusively show that both data are not stationary.

We apply differencing and recheck for stationarity.

data_df['NIA'] = data_df['NIA'] - data_df['NIA'].shift(1)
data_df['Dam'] = data_df['Dam'] - data_df['Dam'].shift(1)
data_df = data_df.dropna()

lag_plots(data_df)

kpss_test(data_df)

adf_test(data_df)

**Result:** All three conclusively agree that both data are now stationary.

We next split the data and select the lag order $p$.

train_df, test_df = splitter(data_df)

select_p(train_df)

We select $p=8$ with the same reasons as before. We finally fit our VAR model and test for Granger Causality.

p = 8
model = VAR(train_df)
var_model = model.fit(p)

granger_causation_matrix(train_df, train_df.columns, p)

**Recall:** If a given p-value is < significance level (0.05), then, the corresponding X series (column) causes the Y (row).

**Result:** For this particular example, we can say that changes in NIA release flow Granger causes changes in the dam water level. Conversely, changes in dam water level also Granger causes changes in the NIA release flow. This is an example of the feedback mentioned in an earlier section above. This means that NIA release flow data improves changes in dam water level prediction performance, and dam water level data also improves changes in NIA release flow prediction performance.

### Example 2: Causality for La Mesa Dam

We now do the same steps for the La Mesa dataset.

lamesa_df = pd.read_csv('../data/La Mesa_dataset.csv', index_col='Time');
lamesa_df = lamesa_df.dropna()
lamesa_df.head()

fig,ax = plt.subplots(4, figsize=(15,8), sharex=True)
plot_cols = ['Rain', 'ONI', 'NIA', 'Dam']
lamesa_df[plot_cols].plot(subplots=True, legend=False, ax=ax)
for a in range(len(ax)):
    ax[a].set_ylabel(plot_cols[a])
ax[-1].set_xlabel('')
plt.tight_layout()
plt.show()

#### Causality between Rainfall and La Mesa Dam Water Level

In this next example, we first consider the Rain and Dam time series.

data_df = lamesa_df.drop(['ONI', 'NIA'], axis=1)
data_df.head()

We first check for stationarity by looking at the lag plots and applying the KPSS and ADF tests.

lag_plots(data_df)

kpss_test(data_df)

adf_test(data_df)

**Result:** All three conclusively show that again Rain is stationary, while Dam is not.

We apply differencing and recheck for stationarity.

data_df['Dam'] = data_df['Dam'] - data_df['Dam'].shift(1)
data_df = data_df.dropna()

lag_plots(data_df)

kpss_test(data_df)

adf_test(data_df)

**Result:** All three conclusively agree that both data are now stationary.

We next split the data and select the lag order $p$.

train_df, test_df = splitter(data_df)

select_p(train_df)

We select $p=7$. We finally fit our VAR model and test for Granger Causality.

p = 7
model = VAR(train_df)
var_model = model.fit(p)

granger_causation_matrix(train_df, train_df.columns, p)

**Recall:** If a given p-value is < significance level (0.05), then, the corresponding X series (column) causes the Y (row).

**Result:** For this particular example, we can say that rainfall Granger causes changes in the dam water level. Conversely, changes in dam water level also Granger causes rainfall. This is another example of feedback. This means that rainfall data improves changes in dam water level prediction performance, and dam water level data also improves rainfall prediction performance.

#### Causality between NIA Release Flow and La Mesa Dam Water Level

In this next example, we now focus on the NIA and Dam time series.

data_df = lamesa_df.drop(['ONI', 'Rain'], axis=1)
data_df.head()

We first check for stationarity by looking at the lag plots and applying the KPSS and ADF tests.

lag_plots(data_df)

kpss_test(data_df)

adf_test(data_df)

**Result:** All three conclusively show that both data are not stationary.

We apply differencing and recheck for stationarity.

data_df['NIA'] = data_df['NIA'] - data_df['NIA'].shift(1)
data_df['Dam'] = data_df['Dam'] - data_df['Dam'].shift(1)
data_df = data_df.dropna()

lag_plots(data_df)

kpss_test(data_df)

adf_test(data_df)

**Result:** All three conclusively agree that both data are now stationary.

We next split the data and select the lag order $p$.

train_df, test_df = splitter(data_df)

select_p(train_df)

We select $p=14$. We finally fit our VAR model and test for Granger Causality.

p = 14
model = VAR(train_df)
var_model = model.fit(p)

granger_causation_matrix(train_df, train_df.columns, p)

**Recall:** If a given p-value is < significance level (0.05), then, the corresponding X series (column) causes the Y (row).

**Result:** We see that, unlike for Ipo Dam, changes in NIA release flow and changes in the dam water level do NOT Granger cause one another for La Mesa Dam. This means that NIA release flow data does NOT improve changes in dam water level prediction performance, and dam water level data also does NOT improve changes in NIA release flow prediction performance.

### Exercises

As exercises, the reader can test for Granger Causality between other pairs of variables from both the Ipo and La Mesa datasets, as well as from the Angat dataset.

### Example 3: Jena Climate Data

We look back at the Jena climate dataset and explore which variables are Granger causal to another.

train_df = pd.read_csv('../data/train_series_datetime.csv',index_col=0).set_index('Date Time')
val_df = pd.read_csv('../data/val_series_datetime.csv',index_col=0).set_index('Date Time')
test_df = pd.read_csv('../data/test_series_datetime.csv',index_col=0).set_index('Date Time')
train_df.index = pd.to_datetime(train_df.index)
val_df.index = pd.to_datetime(val_df.index)
test_df.index = pd.to_datetime(test_df.index)

train_val_df = pd.concat([train_df, val_df])
jena_df = pd.concat([train_df, val_df, test_df])
jena_df.head()

#### Causality between Pressure and Temperature

In this next example, we first consider the p and T time series.

data_df = jena_df.iloc[:,:2]
data_df.head()

We first check for stationarity by looking at the lag plots and applying the KPSS and ADF tests.

lag_plots(data_df)

kpss_test(data_df)

adf_test(data_df)

**Result:** All three conclusively show that both data are not stationary.

We apply differencing and recheck for stationarity.

data_df['p (mbar)'] = data_df['p (mbar)'] - data_df['p (mbar)'].shift(1)
data_df['T (degC)'] = data_df['T (degC)'] - data_df['T (degC)'].shift(1)
data_df = data_df.dropna()

lag_plots(data_df)

kpss_test(data_df)

adf_test(data_df)

**Result:** All three conclusively agree that both data are now stationary.

We next split the data and select the lag order $p$.

train_df, test_df = splitter(data_df)

select_p(train_df)

We select $p=30$. We finally fit our VAR model and test for Granger Causality.

p = 30
model = VAR(train_df)
var_model = model.fit(p)

granger_causation_matrix(train_df, train_df.columns, p)

**Recall:** If a given p-value is < significance level (0.05), then, the corresponding X series (column) causes the Y (row).

**Result:** For this particular example, we can say that changes in pressure Granger causes changes in temperature. Conversely, changes in temperature also Granger causes pressure. This is another example of feedback. This means that pressure data improves changes in temperature prediction performance, and temperature data also improves pressure prediction performance.

## Summary

We have introduced the notion of causality in this chapter, and discussed its implications on time series analysis. We also applied the Granger Causality Test for linear VAR models for several datasets, seeing different examples of causality between the variables explored.

Causality will be revisited in a later chapter, in particular addressing the limitations of the method discussed in this chapter and discussing causality for nonlinear models.

## References

The contents of this notebook are compiled from the following references:

* [Granger, C. (1969). Investigating Causal Relations by Econometric Models and Cross-spectral Methods. Econometrica, 37(3), 424-438.](https://www.jstor.org/stable/1912791)
* [Toda, Hiro Y. & Yamamoto, Taku (1995). Statistical inference in vector autoregressions with possibly integrated processes. Journal of Econometrics, 66(1-2), 225-250.](https://ideas.repec.org/a/eee/econom/v66y1995i1-2p225-250.html)
* [Hood, M., Kidd, Q., & Morris, I. (2008). Two Sides of the Same Coin? Employing Granger Causality Tests in a Time Series Cross-Section Framework. Political Analysis, 16(3), 324-344.](https://www.jstor.org/stable/25791939)
* [Testing for Granger Causality Using Python](https://rishi-a.github.io/2020/05/25/granger-causality.html)


# -*- coding: UTF-8 -*-
#!/usr/bin/python3
import numpy as np

def GENP(A, b):
    '''
    Gaussian elimination with no pivoting.
    % input: A is an n x n nonsingular matrix
    %        b is an n x 1 vector
    % output: x is the solution of Ax=b.
    % post-condition: A and b have been modified.
    '''
    n =  len(A)
    if b.size != n:
        raise ValueError("Invalid argument: incompatible sizes between A & b.", b.size, n)

    for pivot_row in range(n-1):
        for row in range(pivot_row+1, n):
            multiplier = A[row][pivot_row]/A[pivot_row][pivot_row]

            A[row][pivot_row] = multiplier
            for col in range(pivot_row + 1, n):
                A[row][col] = A[row][col] - multiplier*A[pivot_row][col]

            b[row] = b[row] - multiplier*b[pivot_row]

    #print(A)
    #print(b)
    x = np.zeros(n)
    k = n-1
    x[k] = b[k]/A[k,k]
    while k >= 0:
        x[k] = (b[k] - np.dot(A[k,k+1:],x[k+1:]))/A[k,k]
        k = k-1
    return x

def GEPP(A, b):
    '''
    Gaussian elimination with partial pivoting.
    % input: A is an n x n nonsingular matrix
    %        b is an n x 1 vector
    % output: x is the solution of Ax=b.
    % post-condition: A and b have been modified.
    '''
    n =  len(A)
    if b.size != n:
        raise ValueError("Invalid argument: incompatible sizes between A & b.", b.size, n)

    for k in range(n-1):
        maxindex = abs(A[k:,k]).argmax() + k
        if A[maxindex, k] == 0:
            raise ValueError("Matrix is singular.")

        if maxindex != k:
            A[[k,maxindex]] = A[[maxindex, k]]
            b[[k,maxindex]] = b[[maxindex, k]]
        for row in range(k+1, n):
            multiplier = A[row][k]/A[k][k]

            A[row][k] = multiplier
            for col in range(k + 1, n):
                A[row][col] = A[row][col] - multiplier*A[k][col]

            b[row] = b[row] - multiplier*b[k]
    #print(A)
    #print(b)
    x = np.zeros(n)
    k = n-1
    x[k] = b[k]/A[k,k]
    while k >= 0:
        x[k] = (b[k] - np.dot(A[k,k+1:],x[k+1:]))/A[k,k]
        k = k-1
    return x

if __name__ == "__main__":
    '''
    A = np.array([[1.,-1.,1.,-1.],[1.,0.,0.,0.],[1.,1.,1.,1.],[1.,2.,4.,8.]])
    b = np.array([[14.],[4.],[2.],[2.]])
    '''
    #generate matrix A
    A = np.zeros((84,84))
    A[0][0]=6
    A[0][1]=1
    A[83][82]=8
    A[83][83]=6
    col = 0
    for row in range(1, 83):
        A[row][col]  =8
        A[row][col+1]=6
        A[row][col+2]=1
        col=col+1
    #print(A)

    #generate matrix A
    b = np.zeros((84, 1))
    b[0]=7
    b[83]=14
    for row in range(1, 83):
        b[row]=15
    #print(b)

    print("Gaussian elimination with no pivoting.")
    print(GENP(np.copy(A), np.copy(b)))

    print("Gaussian elimination with partial pivoting.")
    print(GEPP(A,b))


'''
Generating meshes

In order to visualize two-dimensional arrays of data, it is necessary to understand how to generate and manipulate 2-D arrays. Many Matplotlib plots support arrays as input and in particular, they support NumPy arrays. The NumPy library is the most widely-supported means for supporting numeric arrays in Python.

In this exercise, you will use the meshgrid function in NumPy to generate 2-D arrays which you will then visualize using plt.imshow(). The simplest way to generate a meshgrid is as follows:

import numpy as np
Y,X = np.meshgrid(range(10),range(20))
This will create two arrays with a shape of (20,10), which corresponds to 20 rows along the Y-axis and 10 columns along the X-axis. In this exercise, you will use np.meshgrid() to generate a regular 2-D sampling of a mathematical function.

INSTRUCTIONS
100XP
Import the numpy and matplotlib.pyplot modules using the respective aliases np and plt.
Generate two one-dimensional arrays u and v using np.linspace(). The array u should contain 41 values uniformly spaced beween -2 and +2. The array v should contain 21 values uniformly spaced between -1 and +1.
Construct two two-dimensional arrays X and Y from u and v using np.meshgrid(). The resulting arrays should have shape (41,21).
After the array Z is computed using X and Y, visualize the array Z using plt.pcolor() and plt.show().
Save the resulting figure as 'sine_mesh.png'.
'''
# Import numpy and matplotlib.pyplot
import numpy as np
import matplotlib.pyplot as plt

# Generate two 1-D arrays: u, v
u = np.linspace(-2, 2, 41)
v = np.linspace(-1, 1, 21)

# Generate 2-D arrays from u and v: X, Y
X,Y = np.meshgrid(u, v)

# Compute Z based on X and Y
Z = np.sin(3*np.sqrt(X**2 + Y**2))

# Display the resulting image with pcolor()
plt.pcolor(Z)
plt.show()

# Save the figure to 'sine_mesh.png'
plt.savefig('sine_mesh.png')

Data Science With Python/13-introduction-to-data-visualization-with-python/02-plotting-2d-arrays/01-generating-meshes.py