import numpy as np

######################################################################################################
# Scalars 																							 #
######################################################################################################
#
# Scalars in NumPy are a bit more involved than in Python.
# Instead of Python’s basic types like int, float, etc., NumPy lets you specify signed and unsigned types, as well as
# different sizes. So instead of Python’s int, you have access to types like uint8, int8, uint16, int16, and so on.

scalar = np.array(5)
print('Scalar = ', scalar, 'shape: ', scalar.shape)

scalarSum = scalar + 11
print('Scalar sum: ', scalar, ' + 11 = ', scalarSum)

######################################################################################################
# Vectors 																							 #
######################################################################################################
#
# To create a vector, you'd pass a Python list to the array function, like this:

vector = np.array([1, 2, 3])
print('Vector: ', vector, 'shape: ', vector.shape)

# Access an element within the vector using indices, like this:
print('vector[1] = ', vector[1])

# NumPy also supports advanced indexing techniques.
# For example, to access the items from the second element onward, you would say:
print(vector[1:])

######################################################################################################
# Matrices 																							 #
######################################################################################################
#
# You create matrices using NumPy's array function, just you did for vectors.
# However, instead of just passing in a list, you need to supply a list of lists, where each list represents a row.
# So to create a 3x3 matrix containing the numbers one through nine, you could do this:

matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print('Matrix: \n', matrix)
print('Matrix shape: ', matrix.shape)
print('matrix[1][2] = ', matrix[1][2])

######################################################################################################
# Tensors 																							 #
######################################################################################################
#
# Tensors are just like vectors and matrices, but they can have more dimensions.
# For example, to create a 3x3x2x1 tensor, you could do the following:

tensor = np.array([[[[1],[2]],[[3],[4]],[[5],[6]]],[[[7],[8]],\
    [[9],[10]],[[11],[12]]],[[[13],[14]],[[15],[16]],[[17],[18]]]])

print('Tensor: \n', tensor)
print('Tensor shape: ', tensor.shape)

# Access items just like with matrices, but with more indices. So t[2][1][1][0] will return 16.
print('tensor[2][1][1][0]', tensor[2][1][1][0])

######################################################################################################
# Element-wise operations																			 #
######################################################################################################
# The Python way
# Suppose you had a list of numbers, and you wanted to add 5 to every item in the list.
# Without NumPy, you might do something like this:

p_values = [1, 2, 3, 4, 5]
for i in range(len(p_values)):
    p_values[i] += 5

# now values holds [6, 7, 8, 9, 10]

# The NumPy way
# In NumPy, we could do the following:

np_values = [1, 2, 3, 4, 5]
np_values = np.array(np_values) + 5

print('[1, 2, 3, 4, 5] + 5 == ', np_values)

# now values is an ndarray that holds [6, 7, 8, 9, 10]

# We should point out, NumPy actually has functions for things like adding, multiplying, etc.
# But it also supports using the standard math operators. So the following two lines are equivalent:

some_array = np.array([1, 2, 3, 4, 5])
np_mult_1 = np.multiply(some_array, 5)
np_mult_2 = some_array * 5

print('[1, 2, 3, 4, 5] * 5 == ', np_mult_2)

# Init with zeros:
zero_matrix = np_mult_2 * 0

# now every element in m is zero, no matter how many dimensions it has
print(np_mult_2, '* 0 == ', zero_matrix)

######################################################################################################
# Element-wise Matrix Operations 																	 #
######################################################################################################
#
#The same functions and operators that work with scalars and matrices also work with other dimensions.
# You just need to make sure that the items you perform the operation on have compatible shapes.
#
# Let's say you want to get the squared values of a matrix.
# That's simply x = m * m (or if you want to assign the value back to m, it's just m *= m
#
# This works because it's an element-wise multiplication between two identically-shaped matrices.
# (In this case, they are shaped the same because they are actually the same object.)
#
# Here's the example:

a = np.array([[1, 3], [5, 7]])
print('A:\n', a)
# displays the following result:
# array([[1, 3],
#        [5, 7]])

b = np.array([[2, 4], [6, 8]])
print('B:\n', b)
# displays the following result:
# array([[2, 4],
#        [6, 8]])

print('A + B:\n', a + b)
# displays the following result
#      array([[ 3,  7],
#             [11, 15]])


# -*- coding: utf-8 -*-
from __future__ import print_function, division
# Import Library of Numpy
import numpy as np

# Version of Numpy
np.__version__

#NumPy is an N-dimensional array type called ndarray
#NumPy rank 1 array
x1 = np.array([1, 2, 3, 4,5])
print ('x1 =', x1)
print ('------------------------------')
print ('type (x1) =', type (x1))
print ('------------------------------')
print ('x1.shape =', x1.shape)
print ('------------------------------')
print ('x1.size =', x1.size) # number of elements
print ('------------------------------')
print (x1[0],',',x1[1],',',x1[2],',',x1[3]) #Prints "1 2 3 4"
print ('------------------------------')
x1[3] = 0
print (x1) # Change an element
#A data type object (an instance of numpy.dtype class) describes how the bytes in the fixed-size block of memory corresponding to an array item should be interpreted.
'''#int8 = Byte (-128 to 127)
#int16 = Integer (-32768 to 32767)
#int32 = Integer (-2147483648 to +2147483647)
#int64 = Integer (-9223372036854775808 to +9223372036854775807)
#Boolean = (True or False) stored as a byte
#float = Shorthand for float64 : Double precision float: sign bit, 11 bits exponent, 52 bits mantissa
#complex = Shorthand for complex128 : Complex number
#int8, int16, int32, int64 can be replaced by equivalent string 'i1', 'i2','i4', etc. '''

x1a = np.array([1, 2, 3, 4] , dtype='float') #Desired data type of array, optional
#x1a = np.array([1, 2, 3, 4] , dtype='i1')
#x1a = np.array([1, 2, 3, 4] , dtype= complex)
print ('x1a =', x1a)
print ('------------------------------')
print ('type (x1a) =', type (x1a))
print ('------------------------------')
print ('x1a.dtype =', x1a.dtype)
print ('------------------------------')
print ('x1a.itemsize =', x1a.itemsize) # 8 byte
print ('------------------------------')
print ('x1a.size =', x1a.size)
print ('------------------------------')
print ('x1a.shape =', x1a.shape)
print ('------------------------------')
x1b = np.array([[.25, 2, 3, 4],[5, 6, 7, 8],[9, 10, 11, 12],[13, 14, 15, 16]])
print (x1b.dtype) #dtype is float
print ('------------------------------')
print ('x1b.shape =', x1b.shape) #(4,4)
print ('------------------------------')
x1ba = x1b [0:2,1:3]
print ('x1ba [0:2,1:3] : ', x1ba)
print ('------------------------------')
print ('x1b [0,2] : ', x1b [0,2])
print ('------------------------------')
x1b [0,2] = 17
print ('x1b = ', x1b)
print ('------------------------------')
print ('x1ba : ', x1ba) # has changed   3=>17
print ('------------------------------')
x1ba = +x1b [0:2,1:3] #x1ba dosen't depend on x1b
x1b [0,2] = 125
print ('------------------------------')
print ('x1b = ', x1b)
print ('------------------------------')
print ('x1ba : ', x1ba) # dosen't change
print ('------------------------------')
x1c = np.array([[.25, 2, 3, 4],[8],[9, 10, 11, 12],[13, 14, 15, 16]])
print ('x1c.shape =', x1c.shape) #(4,)
print ('------------------------------')
x1d = x1a.copy()
print('x1d =',x1d)
print ('------------------------------')
print ('x1d is x1a =', x1d is x1a) # False
print ('------------------------------')
print('x1d.ndim =',x1d.ndim) # dimension

# More than one dimension
#NumPy rank 2 array
x2 = np.array([[1,2,3,4],[4,3,2,1]])
print (x2)
print (type (x2))
print (x2.shape)
print (x2[0,0],',',x2[1,3])

#Return a array setting values to zero
x2a = np.zeros((4,2))
print ('x2a =',x2a)
x2b = np.zeros_like(x2)
print ('------------------------------')
print('x2b =',x2b)
print ('------------------------------')
x2b1 = np.zeros((2,4,2))
print('x2b1 =',x2b1)
print('x2b1.ndim = ',x2b1.ndim)
print ('------------------------------')
#Return a array setting values to one
x2c = np.ones((4,3))
print('x2c =',x2c)
print ('------------------------------')
x2c = np.ones((4,3), dtype='i1')
print('x2c =',x2c)
print ('------------------------------')
x2d = (4,3)
x2e = np.ones(x2d, dtype='int')
print('x2e =',x2e)
print ('------------------------------')

#numpy.full => Return a new array of given shape and type, filled with fill_value.
x2f = np.full((3,3),0)
print('x2f =',x2f)
print ('------------------------------')
x2g = np.full((2,2),1)
print('x2g =',x2g)
print ('------------------------------')
x2h = np.full((7,),6)
print('x2h =',x2h)
print ('------------------------------')
x2i = np.full((5,2),'test')
print('x2i =',x2i)
print ('------------------------------')

#numpy.eye => Return a 2-D array with ones on the diagonal and zeros elsewhere
x2j = np.eye(4)
print('x2j =',x2j)
print ('------------------------------')
x2k = np.eye(2, dtype=int)
print('x2k =',x2k)
print ('------------------------------')
x2l = np.eye(5, k=2, dtype=int)   # k => Index of the diagonal : 0 (the default) refers to the main diagonal
print('x2l =',x2l)
print ('------------------------------')

# array filled with random values
x2m = np.random.random((3,2))
print('x2m =',x2m)
print ('------------------------------')
#Create an array of the given shape and populate it with random samples from a uniform distribution over
x2n = np.random.rand(2,5) # 2,5 are the dimensions of the returned array
print('x2n =',x2n)
print ('------------------------------')
#uniform([low, high, size]) 	Draw samples from a uniform distribution
x2o = np.random.uniform(2,7,(2,2))
print('x2o =',x2o)
print ('------------------------------')
#standard_normal([size]) 	Draw samples from a standard Normal distribution (mean=0, stdev=1)
x2p = np.random.standard_normal((2,2))
print('x2p =',x2p)
print ('------------------------------')

#numpy.arange(start,stop,step, dtype=None)
#Return evenly spaced values within a given interval.
x3 = np.arange(7)
print ('x3 =',x3)
print ('------------------------------')
x3a = np.arange(7.0)
print ('x3a =',x3a)
print ('------------------------------')
x3b = np.arange(4.5)
print ('x3b =',x3b)
print ('------------------------------')
x3c = np.arange(4,19)
print ('x3c =',x3c)
print ('------------------------------')
x3d = np.arange(4,19,3)   # 3 is step
print ('x3d =',x3d)
print ('------------------------------')
x3e = np.arange(4,19,3, dtype=float)
print ('x3e =',x3e)
print ('------------------------------')
x3f = np.arange(4,19,2.4)
print ('x3f =',x3f)
print ('------------------------------')
#numpy.linspace (start, stop, num) => Return evenly spaced numbers over a specified interval
#from numpy import pi
x3g = np.linspace(3.0, 9.0, num=9)
print ('x3g =',x3g)
print ('------------------------------')

# A matrix is a specialized 2-D array that retains its 2-D nature through operations
x3 = np.matrix([[1,2],[3,4]]) # Or x2 = np.matrix('1 2; 3 4')
print(x3)
print (type (x3))
print (x3.shape)
print (x3[1,1], x3[0,1], x3[0,0], x3[1,0])

#Computation on NumPy Arrays
#Dot product of two arrays. Specifically,
#   If both a and b are 1-D arrays, it is inner product of vectors (without complex conjugation).
#   If both a and b are 2-D arrays, it is matrix multiplication, but using matmul or a @ b is preferred.
#  If either a or b is 0-D (scalar), it is equivalent to multiply and using numpy.multiply(a, b) or a * b is preferred.
x4 = np.dot(7,2)
print("x4 = ",x4)
print ('------------------------------')
x5 = np.ones((2,3))
print("x5 = ",x5)
print ('------------------------------')
x6 = np.array([[2,5],[3,7],[9,6]])
x7 = np.dot(x5,x6)
print("x7 = ",x7)
print ('------------------------------')
x8 = x5 @ x6
print("x8 = ",x8)
print ('------------------------------')
x9 = np.array([[2,2],[4,4]])
x10 = np.multiply(x9,x9)
print('x10 = ',x9,'*',x9,' = ',x10)
print ('------------------------------')
print('x9 * x9 = ',x9*x9)
print ('------------------------------')
print('x9 @ x9 = ',x9@x9)
print ('------------------------------')
#numpy.prod => Return the product of array elements over a given axis
x11 = np.prod (x9)
print("x11 = ",x11)
print ('------------------------------')
#The product of an empty array is the neutral element 1
x12 = np.prod([] , dtype='i1')
print("x12 = ",x12)
print ('------------------------------')
# we can also specify the axis over which to multiply
x13 = np.prod([[1, 2, 3],[4, 5, 6],[11, 10, 2]], axis=0)
print("x13 = ",x13)
print ('------------------------------')

#Broadcasting => The term broadcasting describes how numpy treats arrays with different shapes during arithmetic operations
x14 = np.array([1.0, 2.0, 3.0, 4.0])
x15 = x14 + 7
print("x15 = ",x15)
print ('------------------------------')
x16 = np.ones((4,4))
x17 = x14 + x16
print("x17 = ",x17)
print ('------------------------------')
x18 = np.ones((4,1))
x19 = x14 + x18
print("x19 = ",x19)
print ('------------------------------')

#numpy.sum => Sum of array elements over a given axis
x20 = np.sum([1.5, 2.5])
print("x20 = ",x20)
print ('------------------------------')
x21 = np.sum([[2,4],[6,8]])
print("x21 = ",x21)
print ('------------------------------')
x22 = np.sum([[2,4],[6,8]], axis=0)
print("x22 = ",x22)
print ('------------------------------')
x23 = np.sum([[2,4],[6,8]], axis=1)
print("x23 = ",x23)
print ('------------------------------')
#numpy.cumsum => Return the cumulative sum of the elements along a given axis
a = np.array([[1,2,3], [7,8,9]])
x24 = np.cumsum(a)
print("x24 = ",x24)
print ('------------------------------')
x25 = np.cumsum(a, axis=0)  # sum over rows for each of the 3 columns
print("x25 = ",x25)
print ('------------------------------')
x26 = np.cumsum(a, axis=1) # sum over columns for each of the 2 rows
print("x26 = ",x26)
print ('------------------------------')
#numpy.subtract
x27 = np.subtract(a,a)
print("x27 = ",x27)
print ('------------------------------')
#numpy.divide => Returns a true division of the inputs
x28 = np.divide(a,2)
print("x28 = ",x28)
print ('------------------------------')
x29 = np.floor_divide(a,2)
print("x29 = ",x29)
print ('------------------------------')
x30 = np.true_divide(a,2)
print("x30 = ",x30)
print ('------------------------------')

#numpy.math
'''
1- sin(x, /[, out, where, casting, order, ...]) 	Trigonometric sine, element-wise.
2- cos(x, /[, out, where, casting, order, ...]) 	Cosine element-wise.
3- tan(x, /[, out, where, casting, order, ...]) 	Compute tangent element-wise.
4- arcsin(x, /[, out, where, casting, order, ...]) 	Inverse sine, element-wise.
5- arccos(x, /[, out, where, casting, order, ...]) 	Trigonometric inverse cosine, element-wise.
6- arctan(x, /[, out, where, casting, order, ...]) 	Trigonometric inverse tangent, element-wise.
7- hypot(x1, x2, /[, out, where, casting, ...]) 	Given the “legs” of a right triangle, return its hypotenuse.
8- arctan2(x1, x2, /[, out, where, casting, ...]) 	Element-wise arc tangent of x1/x2 choosing the quadrant correctly.
9- degrees(x, /[, out, where, casting, order, ...]) 	Convert angles from radians to degrees.
10- radians(x, /[, out, where, casting, order, ...]) 	Convert angles from degrees to radians.
11- unwrap(p[, discont, axis]) 	Unwrap by changing deltas between values to 2*pi complement.
12- deg2rad(x, /[, out, where, casting, order, ...]) 	Convert angles from degrees to radians.
13- rad2deg(x, /[, out, where, casting, order, ...]) 	Convert angles from radians to degrees.
14- Hyperbolic functions
15- sinh(x, /[, out, where, casting, order, ...]) 	Hyperbolic sine, element-wise.
16- cosh(x, /[, out, where, casting, order, ...]) 	Hyperbolic cosine, element-wise.
17- tanh(x, /[, out, where, casting, order, ...]) 	Compute hyperbolic tangent element-wise.
18- arcsinh(x, /[, out, where, casting, order, ...]) 	Inverse hyperbolic sine element-wise.
19- arccosh(x, /[, out, where, casting, order, ...]) 	Inverse hyperbolic cosine, element-wise.
20- arctanh(x, /[, out, where, casting, order, ...]) 	Inverse hyperbolic tangent element-wise.
21- Rounding
22- around(a[, decimals, out]) 	Evenly round to the given number of decimals.
23- round_(a[, decimals, out]) 	Round an array to the given number of decimals.
24- rint(x, /[, out, where, casting, order, ...]) 	Round elements of the array to the nearest integer.
25- fix(x[, out]) 	Round to nearest integer towards zero.
26- floor(x, /[, out, where, casting, order, ...]) 	Return the floor of the input, element-wise.
27- ceil(x, /[, out, where, casting, order, ...]) 	Return the ceiling of the input, element-wise.
28- trunc(x, /[, out, where, casting, order, ...]) 	Return the truncated value of the input, element-wise.
29- Sums, products, differences
30- prod(a[, axis, dtype, out, keepdims]) 	Return the product of array elements over a given axis.
31- sum(a[, axis, dtype, out, keepdims]) 	Sum of array elements over a given axis.
32- nanprod(a[, axis, dtype, out, keepdims]) 	Return the product of array elements over a given axis treating Not a Numbers (NaNs) as ones.
33- nansum(a[, axis, dtype, out, keepdims]) 	Return the sum of array elements over a given axis treating Not a Numbers (NaNs) as zero.
34- cumprod(a[, axis, dtype, out]) 	Return the cumulative product of elements along a given axis.
35- cumsum(a[, axis, dtype, out]) 	Return the cumulative sum of the elements along a given axis.
36- nancumprod(a[, axis, dtype, out]) 	Return the cumulative product of array elements over a given axis treating Not a Numbers (NaNs) as one.
37- nancumsum(a[, axis, dtype, out]) 	Return the cumulative sum of array elements over a given axis treating Not a Numbers (NaNs) as zero.
38- diff(a[, n, axis]) 	Calculate the n-th discrete difference along given axis.
39- ediff1d(ary[, to_end, to_begin]) 	The differences between consecutive elements of an array.
40- gradient(f, *varargs, **kwargs) 	Return the gradient of an N-dimensional array.
41- cross(a, b[, axisa, axisb, axisc, axis]) 	Return the cross product of two (arrays of) vectors.
42- trapz(y[, x, dx, axis]) 	Integrate along the given axis using the composite trapezoidal rule.
43- Exponents and logarithms
44- exp(x, /[, out, where, casting, order, ...]) 	Calculate the exponential of all elements in the input array.
45- expm1(x, /[, out, where, casting, order, ...]) 	Calculate exp(x) - 1 for all elements in the array.
46- exp2(x, /[, out, where, casting, order, ...]) 	Calculate 2**p for all p in the input array.
47- log(x, /[, out, where, casting, order, ...]) 	Natural logarithm, element-wise.
48- log10(x, /[, out, where, casting, order, ...]) 	Return the base 10 logarithm of the input array, element-wise.
49- log2(x, /[, out, where, casting, order, ...]) 	Base-2 logarithm of x.
50- log1p(x, /[, out, where, casting, order, ...]) 	Return the natural logarithm of one plus the input array, element-wise.
51- logaddexp(x1, x2, /[, out, where, casting, ...]) 	Logarithm of the sum of exponentiations of the inputs.
52- logaddexp2(x1, x2, /[, out, where, casting, ...]) 	Logarithm of the sum of exponentiations of the inputs in base-2.
53- Other special functions
54- i0(x) 	Modified Bessel function of the first kind, order 0.
55- sinc(x) 	Return the sinc function.
56- Floating point routines
57- signbit(x, /[, out, where, casting, order, ...]) 	Returns element-wise True where signbit is set (less than zero).
58- copysign(x1, x2, /[, out, where, casting, ...]) 	Change the sign of x1 to that of x2, element-wise.
59- frexp(x[, out1, out2], / [[, out, where, ...]) 	Decompose the elements of x into mantissa and twos exponent.
60- ldexp(x1, x2, /[, out, where, casting, ...]) 	Returns x1 * 2**x2, element-wise.
61- nextafter(x1, x2, /[, out, where, casting, ...]) 	Return the next floating-point value after x1 towards x2, element-wise.
62- spacing(x, /[, out, where, casting, order, ...]) 	Return the distance between x and the nearest adjacent number.
63- Arithmetic operations
64- add(x1, x2, /[, out, where, casting, order, ...]) 	Add arguments element-wise.
65- reciprocal(x, /[, out, where, casting, ...]) 	Return the reciprocal of the argument, element-wise.
66- negative(x, /[, out, where, casting, order, ...]) 	Numerical negative, element-wise.
67- multiply(x1, x2, /[, out, where, casting, ...]) 	Multiply arguments element-wise.
68- divide(x1, x2, /[, out, where, casting, ...]) 	Divide arguments element-wise.
69- power(x1, x2, /[, out, where, casting, ...]) 	First array elements raised to powers from second array, element-wise.
70- subtract(x1, x2, /[, out, where, casting, ...]) 	Subtract arguments, element-wise.
71- true_divide(x1, x2, /[, out, where, ...]) 	Returns a true division of the inputs, element-wise.
72- floor_divide(x1, x2, /[, out, where, ...]) 	Return the largest integer smaller or equal to the division of the inputs.
73- float_power(x1, x2, /[, out, where, ...]) 	First array elements raised to powers from second array, element-wise.
74- fmod(x1, x2, /[, out, where, casting, ...]) 	Return the element-wise remainder of division.
75- mod(x1, x2, /[, out, where, casting, order, ...]) 	Return element-wise remainder of division.
76- modf(x[, out1, out2], / [[, out, where, ...]) 	Return the fractional and integral parts of an array, element-wise.
77- remainder(x1, x2, /[, out, where, casting, ...]) 	Return element-wise remainder of division.
78- divmod(x1, x2[, out1, out2], / [[, out, ...]) 	Return element-wise quotient and remainder simultaneously.
79- Handling complex numbers
80- angle(z[, deg]) 	Return the angle of the complex argument.
81- real(val) 	Return the real part of the complex argument.
82- imag(val) 	Return the imaginary part of the complex argument.
83- conj(x, /[, out, where, casting, order, ...]) 	Return the complex conjugate, element-wise.
84- Miscellaneous
85- convolve(a, v[, mode]) 	Returns the discrete, linear convolution of two one-dimensional sequences.
86- clip(a, a_min, a_max[, out]) 	Clip (limit) the values in an array.
87- sqrt(x, /[, out, where, casting, order, ...]) 	Return the positive square-root of an array, element-wise.
88- cbrt(x, /[, out, where, casting, order, ...]) 	Return the cube-root of an array, element-wise.
89- square(x, /[, out, where, casting, order, ...]) 	Return the element-wise square of the input.
90- absolute(x, /[, out, where, casting, order, ...]) 	Calculate the absolute value element-wise.
91- fabs(x, /[, out, where, casting, order, ...]) 	Compute the absolute values element-wise.
92- sign(x, /[, out, where, casting, order, ...]) 	Returns an element-wise indication of the sign of a number.
93- heaviside(x1, x2, /[, out, where, casting, ...]) 	Compute the Heaviside step function.
94- maximum(x1, x2, /[, out, where, casting, ...]) 	Element-wise maximum of array elements.
95- minimum(x1, x2, /[, out, where, casting, ...]) 	Element-wise minimum of array elements.
96- fmax(x1, x2, /[, out, where, casting, ...]) 	Element-wise maximum of array elements.
97- fmin(x1, x2, /[, out, where, casting, ...]) 	Element-wise minimum of array elements.
98- nan_to_num(x[, copy]) 	Replace nan with zero and inf with finite numbers.
99- real_if_close(a[, tol]) 	If complex input returns a real array if complex parts are close to zero.
100- interp(x, xp, fp[, left, right, period]) 	One-dimensional linear interpolation.'''
#numpy.reshape => Gives a new shape to an array without changing its data
x31 = np.arange(8).reshape(4,2) # 4*2 = 8 (number of elements)
print("x31 = ",x31)
print ('------------------------------')
x31a = np.arange(4).reshape(4) # reshape(4) => number of columns
print("x31a = ",x31a)
print ('------------------------------')
x32 = np.math.inf #IEEE 754 floating point representation of (positive) infinity.
print("x32 = ",x32)
print ('------------------------------')
#x33 = np.array([1]) / 0
#print("x33 = ",x33)
#print ('------------------------------')
x34 = np.math.nan #not a number
print("x34 = ",x34)
print ('------------------------------')

#Mask => Logic functions
'''
Logical operations:
logical_and(x1, x2, /[, out, where, ...]) 	Compute the truth value of x1 AND x2 element-wise.
logical_or(x1, x2, /[, out, where, casting, ...]) 	Compute the truth value of x1 OR x2 element-wise.
logical_not(x, /[, out, where, casting, ...]) 	Compute the truth value of NOT x element-wise.
logical_xor(x1, x2, /[, out, where, ...]) 	Compute the truth value of x1 XOR x2, element-wise.'''

x35 = np.arange(18).reshape(3,2,3) # 3*2*3 = 9 (number of elements)
print("x35 = ",x35)
print("x35.ndim = ",x35.ndim) # ndim = 3
print ('------------------------------')
x35a = np.arange(24).reshape(4,2,3)
print("x35a = ",x35a)
print("x35a.ndim = ",x35a.ndim) # ndim = 3
print ('------------------------------')
x35b = np.arange(40).reshape(2,5,4)
print("x35b = ",x35b)
print("x35b.ndim = ",x35b.ndim) # ndim = 3
print ('------------------------------')
x35c = np.arange(20).reshape(1,5,4)
print("x35c = ",x35c)
print("x35c.ndim = ",x35c.ndim) # ndim = 3
print ('------------------------------')
x36 = x35 < 4
print("x36 = ",x36)
print ('------------------------------')
x37 = x35 [x36]
print("x37 = ",x37)
print ('------------------------------')
x38 = np.logical_and(x35>=2 , x35<5)
print("x38 = ",x38)
print ('------------------------------')
x39 = x35 [x38]
print("x39 = ",x39)
print ('------------------------------')

#other
#NumPy.unique => Find the unique elements of an array
x40 = np.unique([1, 1, 2, 2, 3, 3,4,4,4,5])
print("x40 = ",x40)
print ('------------------------------')
x41 = np.unique(np.array(([[1, 1], [2, 2], [3,2], [4,5]])))
print("x41 = ",x41)
print ('------------------------------')
x42 = np.unique(np.array([[1, 2, 3], [1, 2, 3], [2, 3, 4]]), axis=0) #Return the unique rows of a 2D array
print("x42 = ",x42)
print ('------------------------------')
#numpy.union1d =>  Find the union of two arrays
a = np.arange(12,27,2).reshape(2,4)
print("a = ",a)
print ('------------------------------')
b = np.ones((2,3), dtype='1i')
print("b = ",b)
print ('------------------------------')
c = np.arange(1,7).reshape(3,2)
print("c = ",c)
print ('------------------------------')
x43 = np.union1d(b,c)
print("x43 = ",x43)
print ('------------------------------')
# find the union of more than two arrays, use functools.reduce
from functools import reduce
x44 = reduce(np.union1d, (a,b,c))
print("x44 = ",x44)
print ('------------------------------')
#numpy.intersect1d => Find the intersection of two arrays
x45 = np.intersect1d(b,c)
print("x45 = ",x45)
print ('------------------------------')
x46 = reduce(np.intersect1d, ([1, 3, 5, 3], [6, 1, 3, 1], [7, 3, 1, 2]))
print("x46 = ",x46)
print ('------------------------------')

# numpy.sort => Return a sorted copy of an array
x47 = np.array([[1,4,2,2],[3,1,4,5],[8,8,7,20]])
x48 = np.sort(x47)
print("x48 = ",x48)
print ('------------------------------')
x49 = np.sort(x47, axis=0)
print("x49 = ",x49)
print ('------------------------------')
x50 = np.sort(x47, axis=1)
print("x50 = ",x50)
print ('------------------------------')
x51 = np.sort(x47, axis=None)
print("x51 = ",x51)
print ('------------------------------')
#Using tuple for numpy.ndarray
x52 = np.array ((2,3,4,5))
print("x52 = ",x52)
print ('------------------------------')
x53 = np.array (((2,3,4,5),(5,6,8,7)))
print("x53 = ",x53)
print ('------------------------------')
print("type (x53) : ",type (x53))

#numpy.vstack => Stack arrays in sequence vertically (row wise)
x54 = np.array([2, 4, 6])
x55 = np.array([1, 3, 7])
x56 = np.vstack((x54,x55)) #The numbe of column (x54,x55) is same
#x56 = np.vstack([x54,x55])
print("x56 = ",x56)
print ('------------------------------')
x57 = np.vstack((x55,x54))
print("x57 = ",x57)
print ('------------------------------')
#numpy.hstack => Stack arrays in sequence horizontally (column wise)
x58 = np.hstack((x54,x55))
print("x58 = ",x58)

# numpy.ndarray and (for    in  )
x59 = np.array([[2,85,79,34],[5,78,36,44],[12,0,1,99]])
print('x59 = ')
for x in x59:   #show the rows separately
    print(x)
print ('------------------------------')
x60 = x59.reshape(12)
print('x60 = ')
for x in x60:
    print(x)
print ('------------------------------')
#numpy.ravel => Return a contiguous flattened array.
print('x59 = ')
for x in np.ravel(x59):  #for x in x60.ravel()
    print(x)
print ('------------------------------')
#Iterating Over Arrays
print('x59 = ')
for x in np.nditer(x59):
    print (x)
print ('------------------------------')
x61 = iter(x59)
for x in x59:
    print ('next(x61) = ',next(x61))
    print ('.............')
print ('------------------------------')
x62 = iter(x59)
for x in x59:
    y = next(x62)
    for z in y:
        print (z)

x63 = np.mean (x59)
print("mean = ",x63)
print ('------------------------------')
x64 = np.var (x59)
print("var = ",x64)
print ('------------------------------')
x65 = np.std (x59)
print("std = ",x65)
print ('------------------------------')
x66 = np.median (x59)
print("median = ",x66)
print ('------------------------------')

#numpy.polyval(p, x) => Evaluate a polynomial at specific values
# ax^2+bx+c = 0
# 2x^2+x+3 = 0
x67 = np.array([2,1,3])
print ('x=2 => 2x^2+x+3 = 0, ', np.polyval(x67,2))
print ('------------------------------')
# numpy.polyder => Return the derivative of the specified order of a polynomial
print ('np.polyder(x67) : ', np.polyder(x67))
print ('------------------------------')
#numpy.polyint => Return an antiderivative (indefinite integral) of a polynomial
print ('np.polyint(x67) : ', np.polyint(x67))
print ('------------------------------')

# Comparing Lists and Arrays

# Make a list
L_1 = [1, 2, 3, 4]
# Make Equivalent Array
A_1 = np.array ([1, 2, 3, 4])

# print reverse of list
L_1.reverse()

#L_1 = L_1[::-1]

for e in L_1:
	print (e)

print ('-----------------')

# reverse the array
A_1 = A_1[::-1]

for i in A_1:
	print (i)

print ('-----------------')

# insert an element into list
L_1.append(5)
print (L_1)

print ('-----------------')

#A_1.append(5)   # Error
#print A_1

#A_1 = A_1 + [6,7] # eRROR
#print A_1

#print ('-----------------')

L_1 = L_1 + [6,7]
print (L_1)

print ('-----------------')

L_1 =  L_1 + L_1
print (L_1)

L_2 = []
for i in L_1:
	L_2.append(i+i)
print (L_2)

print ('-----------------')

A_1 = A_1+A_1
print (A_1)

print ('-----------------')

A_1 = A_1 * 2
print (A_1)

L_1 = L_1 * 2
print (L_1)

print ('-----------------')

L_1 = [1, 2, 3, 4]
#L_1 = L_1 **2 # Error
#print (L_1)

L_3 = []
for i in L_1:
	L_3.append(i * i)
print (L_3)

print ('-----------------')

A_1 = np.array ([1, 2, 3, 4])
A_1 = A_1 ** 2
print(A_1)

print ('-----------------')

print (np.sqrt(A_1))
print (np.log(A_1))
print (np.exp(A_1))

# for the list u need use a for loop
print ('-----------------')

x = np.array([1,2])
y = np.array([2,1])

dot = 0

for i, j in zip(x, y):
	dot += (i*j)
print(dot)

print (x*y)  # * ----> element by element multiplication

print (np.sum(x*y))
print ((x*y).sum())
print (np.dot(x,y))  #   dot ------> matrix multiplication
# angle between x, y
angle = np.arccos(x.dot(y)/(np.linalg.norm(x) * np.linalg.norm(y)))
print (angle)

x = np.array([[1,2,3],[4,5,6]])
y = np.array([[7,8],[9,10],[11,12]])

print(x.dot(y))
print (np.dot(x,y))
print(y.dot(x))
# print (x*y)
# print (np.sum(x*y))
# print ((x*y).sum())
print ('-----------------')

# Magnitude of the vector:  np.sqrt((x*x).sum())
print (np.linalg.norm(x))

print ('-----------------')

# list of lists
L = [[1, 2], [3, 4]]

A = np.array ([[4, 5], [6, 7]])

print(L[0])
print(L[0][0])
print(A[0][0])
print(A[0,0])

A_2 = np.matrix(L)
print(A_2)
A_3 = np.array(A_2)
print (A_3)

print ('-----------------')

# transpose

print (A_3.T)

# matrix is -----> two-dimensional numpy array
# vector is -----> one-dimensional array

print ('-----------------')

Z_a = np.zeros(14)
print (Z_a)

Z_a = np.zeros((14, 14))
print (Z_a)

Z_a = np.ones((14, 14))
print (Z_a)

Z_r = np.random.random ((14, 14))
print (Z_r)

#Z_r = np.random.randn ((14, 14))
#print (Z_r)

Z_r = np.random.randn (14, 14)
print (Z_r)

print (Z_r.mean())

print (Z_r.var())

print ('-----------------')

#inverse
A_4 = np.array([[2, 4], [1, 3]])
print (A_4)
A_4_inv = np.linalg.inv(A_4)
print(A_4_inv)
print (np.linalg.det(A_4))
print (np.diag(A_4))

# 1, 2 go in the diagonal and everything else is 0.
print (np.diag ([8, 9]))

# Compute the outer/inner product of two vectors
x = np.array([4, 8])
y = np.array([16, 32])
print (np.outer(x, y))
print (np.inner(x, y))
print (x.dot(y))
print (np.diag(A_4).sum())
print (np.trace(A_4))

print ('-----------------')

X_r = np.random.randn(100, 4)  # 100 samples and 4 features
cov = np.cov(X_r)
print (cov.shape)

# covariance <--------> transpose
cov = np.cov(X_r.T)   # 4 * 4
print (cov)

print ('-----------------')

#Covariance is a Symmetricmatrix
#eigenvalues, eigenvectors = np.eig(x) or np.eigh(x)
#eigh -------> symmetric and Hermitian matrix
#Symmetric means A = A**T
#Hermitian means A = A**H
#A**H = conjugate transpose of A

print (np.linalg.eigh(cov))
print (np.linalg.eig(cov))

print ('-----------------')


import numpy as np
import matplotlib.pyplot as plt

N = 50

x_start, x_end = -2.0, 2.0
y_start, y_end = -1.0, 1.0

x = np.linspace(x_start, x_end, N)
y = np.linspace(y_start, y_end, N)

X, Y = np.meshgrid(x, y)

gamma = 5.0
x_vortex, y_vortex = 0.0, 0.0

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

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

    return u, v

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

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

    return psi

u_vortex, v_vortex = get_velocity_vortex(gamma, x_vortex, y_vortex, X, Y)

psi_vortex = get_stream_function_vortex(gamma, x_vortex, y_vortex, X, Y)

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

plt.show()

strength_sink = -1.0
x_sink, y_sink = 0.0, 0.0

def get_velocity_sink(strength, xs, ys, X, Y):

    u = strength / (2 * np.pi) * (X - xs) / ((X - xs)**2 + (Y - ys)**2)
    v = strength / (2 * np.pi) * (Y - ys) / ((X - xs)**2 + (Y - ys)**2)

    return u, v

def get_stream_function_sink(strength, xs, ys, X, Y):

    psi = strength / (2 * np.pi) * np.arctan2((Y - ys), (X - xs))

    return psi

u_sink, v_sink = get_velocity_sink(strength_sink, x_sink, y_sink, X, Y)

psi_sink = get_stream_function_sink(strength_sink, x_sink, y_sink, X, Y)

u = u_vortex + u_sink
v = v_vortex + v_sink
psi = psi_vortex + psi_sink

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

plt.show()


import copy
import generators
import math
import neighbor_states as ns
import numpy as np
import plotter
import random as rand

def get_matrix(cities):
    n = len(cities)
    result = np.zeros((n, n))

    for x in range(n):
        for y in range(n):
            x1 = cities[x][0]
            y1 = cities[x][1]
            x2 = cities[y][0]
            y2 = cities[y][1]
            distance = np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
            result[x][y] = distance
            result[y][x] = distance
    return result

def path_distance(path, distances):
    distance = 0
    for i in range(len(path) - 1):
        distance += distances[path[i][2]][path[i + 1][2]]
    return distance

def travelling_salesman_problem(n, iterations, temperature, decay_rate, swap_type, low, high, distribution):
    if distribution == "uniform":
        path = generators.get_uniform_distribution_points(low, high, n)
    elif distribution == "normal" or distribution == "gaussian":
        path = generators.get_normal_distribution_points(low, high, n)
    elif distribution == "groups" or distribution == "9 groups":
        path = generators.get_9_groups_of_points(low, high, n)
    else:
        raise ValueError("Error: distribution argument was not uniform, normal or groups!")

    first_path = copy.copy(path)

    if swap_type != "consecutive" and swap_type != "arbitrary":
        raise ValueError("Error: swap type argument was not consecutive or arbitrary!")

    distances = get_matrix(path)

    rand.shuffle(path)

    best_path = path
    min_distance = path_distance(best_path, distances)

    iters = []
    dists = []
    temperatures = []

    for i in range(iterations):
        iters.append(i)

        new_path = copy.copy(path)
        if swap_type == "consecutive":
            new_path = ns.consecutive_swap(new_path)
        else:
            new_path = ns.consecutive_swap(new_path)

        old_path_distance = path_distance(path, distances)
        new_path_distance = path_distance(new_path, distances)

        dists.append(new_path_distance)

        if new_path_distance < old_path_distance:
            path = new_path
            if new_path_distance < min_distance:
                min_distance = new_path_distance
        elif math.exp(-(new_path_distance - old_path_distance)/temperature) > rand.uniform(0, 1):
            path = new_path

        temperatures.append(temperature)
        temperature *= decay_rate

    distances_plot_data = (copy.copy(iters), dists)
    temperatures_plot_data = (iters, temperatures)
    return first_path, best_path, distances_plot_data, temperatures_plot_data

n = 200
iterations = 500000
temperature = 1000
decay_rate = 0.99995
swap_type = "consecutive"
low = 0
high = 1
distribution = "uniform"

first_path, best_path, distances_plot_data, temperatures_plot_data = travelling_salesman_problem(n, iterations, temperature, decay_rate, swap_type, low, high, distribution)

plotter.plot_data(first_path, best_path, distances_plot_data, temperatures_plot_data)


import math
import numpy as np

def norm(vec: np.ndarray):
    return math.sqrt(math.pow(vec[0], 2) + math.pow(vec[1], 2) + math.pow(vec[2], 2))

def dot(vec: np.ndarray, vec2: np.ndarray):
    return vec[0] * vec2[0] + vec[1] * vec2[1] + vec[2] * vec2[2]

def normalize(vec: np.ndarray):
    """Returns a normalized vector of norm 1."""
    return vec / max(norm(vec), 1e-8)

def normalize_batch(vec: np.ndarray):
    return vec / np.maximum(np.linalg.norm(vec, axis=-1), 1e-8)


import pytest

import functools

import numpy as np
from autograd import deriv

import fronts.D

def van_genuchten_D(theta, m, l, alpha, Ks, theta_range):
    # Reference: Van Genuchten (1980) Equation 11
    # https://doi.org/10.2136/sssaj1980.03615995004400050002x

    Se = (theta - theta_range[0])/(theta_range[1] - theta_range[0])

    return (1-m)*Ks/(alpha*m*(theta_range[1] - theta_range[0])) * Se**l*Se**(-1/m) * ((1-Se**(1/m))**(-m) + (1-Se**(1/m))**m - 2)

@pytest.mark.parametrize('theta', (0.5, np.array([0.124, 0.653])))
def test_van_genuchten(theta):

    params = {
        'Ks': 42,
        'alpha': 0.0123,
        'l': 0.27,
        'm': 0.3142,
        'theta_range': (0.123, 0.654)
    }

    D = fronts.D.van_genuchten(**params)

    assert np.all(D(theta, 1)[0] == D(theta))
    assert np.all(D(theta, 2)[0] == D(theta))
    assert np.all(D(theta, 1)[1] == D(theta, 2)[1])

    D_ref = functools.partial(van_genuchten_D, **params)

    assert D(theta) == pytest.approx(D_ref(theta))
    assert D(theta, 1)[1] == pytest.approx(deriv(D_ref)(theta))
    assert D(theta, 2)[2] == pytest.approx(deriv(deriv(D_ref))(theta), rel=1e-5)


import json
import math
import numpy as np

def compute_z(x, y):
    theta = np.array([x, y, 0])
    p = np.exp(theta) / np.sum(np.exp(theta))
    return p[1]

n = 111
m = 111

xs = np.linspace(-3, 3, n)
ys = np.linspace(-3, 3, m)

X, Y = np.meshgrid(xs, ys)
Z = np.empty_like(X)

for i in range(n):
    for j in range(m):
        x = X[i, j]
        y = Y[i, j]
        Z[i, j] = compute_z(x, y)

points = np.column_stack([X.ravel(), Y.ravel(), Z.ravel()])

# by default, this point cloud would be displayed with Z facing up.
# let's rotate it to make it prettier

def rotate3d_x(points, theta):
    c = math.cos(theta)
    s = np.sin(theta)

    R = np.array([[1, 0, 0],
                  [0, c, -s],
                  [0, s, c]])
    return np.dot(points, R)

def rotate3d_z(points, theta):
    c = math.cos(theta)
    s = np.sin(theta)

    R = np.array([[c, -s, 0],
                  [s, c, 0],
                  [0, 0, 1]])
    return np.dot(points, R)

points = rotate3d_z(points, np.pi / 4)
points = rotate3d_x(points, np.pi / 2.5)

data = {
    'x': points[:, 0].tolist(),
    'y': points[:, 1].tolist(),
    'z': points[:, 2].tolist()
}

template = f"""\
var n = {n};
var m = {m};
var data = {json.dumps(data)};
"""

with open('data.js', 'w')  as f:
    print(template, file=f)


![Callysto.ca Banner](https://github.com/callysto/curriculum-notebooks/blob/master/callysto-notebook-banner-top.jpg?raw=true)

# Fibonacci Numbers

The **Fibonacci** sequence is the set of numbers that starts out like this:

$$0,1,1,2,3,5,8,13,\ldots.$$

It's easy to recognize the pattern here. Each number is the sum of the previous two numbers in the sequence. Except, of course, the first two number, 0 and 1, which we put in there to get things started.

This sequence, or pattern of numbers, goes on forever.

These numbers are most commonly known as the **Fibonacci numbers**, after the Italian mathematician **L. Fibonacci** (c. 1200 C.E.). However, these numbers were actually first described hundreds of years before, by Indian mathematicians. The first such mathematician for whom we have written records was **Virahanka** (c. 700 C.E.).

It is difficult to overcome the usage of a name adopted hundreds of years ago, and so in what follows we will refer to the Virahanka-Fibonacci numbers as the "Fibonacci numbers."

**Exercise 1:** Check out the following article about the "so-called Fibonacci" numbers in ancient India: https://www.sciencedirect.com/science/article/pii/0315086085900217

For an excellent exposition about the motivation, poetry, and linguistics of these early mathematicians, and to learn about some fascinating properties of these numbers, check out the following video:

from IPython.display import YouTubeVideo
YouTubeVideo('LP253wHIoO8', start=2633)

It's convenient to label these numbers, so we write $F_0 = 0$, $F_1 = 1$ $F_2 = 2$ and so on. The list of numbers is thus  defined **recursively**  by the formula
$$ \qquad$$
$$ F_n = F_{n-1} + F_{n-2}.$$

We can check the few numbers in the Fibonacci sequence are obtained by that formula, by computing:

$$\begin{eqnarray*}
F_{2} &=&F_{1}+F_{0}=1+0=1 \\
F_{3} &=&F_{2}+F_{1}=1+1=2 \\
F_{4} &=&F_{3}+F_{2}=2+1=3 \\
F_{5} &=&F_{4}+F_{3}=3+2=5 \\
F_{6} &=&F_{5}+F_{4}=5+3=8 \\
F_{7} &=&F_{6}+F_{5}=8+5=13 \\
&&\vdots
\end{eqnarray*}
$$

Here is a list of the first 40 Fibonacci numbers:

$$
\begin{array}{rrrrrrrrrrrrrrr}
0 &  & 1 &  & 1 &  & 2 &  & 3 &  & 5 &  & 8 &  & 13 \\
&  &  &  &  &  &  &  &  &  &  &  &  &  &  \\
21 &  & 34 &  & 55 &  & 89 &  & 144 &  & 233 &  & 377 &  & 610 \\
&  &  &  &  &  &  &  &  &  &  &  &  &  &  \\
987 &  & 1597 &  & 2584 &  & 4181 &  & 6765 &  & 10946 &  & 17711 &  & 28657
\\
&  &  &  &  &  &  &  &  &  &  &  &  &  &  \\
46368 &  & 75025 &  & 121393 &  & 196418 &  & 317811 &  & 614229 &  & 832040
&  & 1346269 \\
&  &  &  &  &  &  &  &  &  &  &  &  &  &  \\
2178309 &  & 3524578 &  & 5702887 &  & 9227465 &  & 14930352 &  & 24157817 &
& 39088169 &  & 63245986%
\end{array}$$

**Exercise 2:** Write a code that computes the first N Fibonacci numbers, saves them into an array, and displays them on the screen.

**WAIT** -- before you read the next cell, try to do Exercise 2!

N = 20   # Set the size of the list we will compute

F=[0,1]  # The first two numbers in the list
for i in range(2, N):
    F.append(F[i-1]+F[i-2])  # append the next item on the list

print('First',N,'Fibonacci numbers:',F)

**For fun,** we can make a little widget to control how many numbers to print out.

from ipywidgets import interact

def printFib(N=10):
    F=[0,1]  # The first two numbers in the list
    for i in range(2, N):
        F.append(F[i-1]+F[i-2])  # append the next item on the list
    print(F)

interact(printFib, N=(10,100,10));

By moving the slider above, print out the first 100 Fibonacci numbers

As we can see, this sequence grows pretty fast. The Fibonacci numbers seem
to have one more digit after about every five terms in the sequence.

## How fast does it grow?

One of the ways to study the growth of a sequence is to look at ratios between consecutive terms. We  look at ratios of pairs of numbers in the Fibonacci sequence.

The first few values are
\begin{eqnarray}
F_2/F_1 &=& 1 \\
F_3/F_2 &=& 2/1 = 2 \\
F_4/F_3 &=& 3/2 = 1.5 \\
F_5/F_4 &=& 5/3 = 1.666... \\
F_6/F_5  &=& 8/5 = 1.6 \\
F_7/F_6  &=& 13/8 = 1.625
\end{eqnarray}

So the ratios are levelling out somewhere around 1.6. We observe that $1.6^5 \approx 10$, which is why after every five terms in the Fibonacci sequence, we get another digit. This tells us we have roughly  **exponential growth,** where $F_n$ grows about as quickly as the exponential function $(1.6)^n$.

We can check this computation in Python. We use $ ** $ to take a power, as in the following cell.

(1.6)**5

## The Golden Ratio

We can print out a bunch of these ratios, and plot them, just to see that they do. The easiest way to do this is with a bit of Python code. Perhaps you can try this yourself.

**Exercise 3** Write some code that computes the first N ratios $F_{n+1}/F_n$, save them it into an array, and displays them on the screen.

**WAIT!** Don't read any further until you try the exercises.

%matplotlib inline
from matplotlib.pyplot import *

N = 20
F = [0,1]
R = []
for i in range(2, N):
    F.append(F[i-1]+F[i-2])  # append the next item on the list
    R.append(F[i]/F[i-1])

figure(figsize=(10,6));
plot(R,'o')
title('The first '+str(N-2)+' Ratios $F_{n+1}/F_n$')
xlabel('$n$')
ylabel('$Ratio$');

print('The first', N-2, 'ratios are:',R)

We see the numbers are levelling out at the value 1.6108034...  This number may be familiar to you. It is called the **Golden Ratio.**

We can compute the exact value by observing the ratios satisfy a nice algebraic equation:
$$
\frac{F_{n+2}}{F_{n+1}}=\frac{F_{n+1}+F_{n}}{F_{n+1}}=1+\frac{F_{n}}{F_{n+1}}=1+\frac{1}{\frac{F_{n+1}}{F_{n}}},
$$
or more simply
$$\frac{F_{n+2}}{F_{n+1}}=1+\frac{1}{\frac{F_{n+1}}{F_{n}}}.$$

As $n$ gets larger and larger, the ratios $F_{n+2}/F_{n+1}$ and $F_{n+1}/F_{n}$ tend toward a final value, say $x$. This value must then solve the equation
$$x=1+\frac{1}{x}.$$

We rewrite this as a quadratic equation
$$x^2=x+1$$
which we solve from the quadratic formula
$$ x= \frac{1 \pm \sqrt{1+4}}{2} = \frac{1 \pm \sqrt{5}}{2}.$$
It is the positive solution $x= \frac{1 + \sqrt{5}}{2} = 1.6108034...$ which is called the Golden Ratio.

The **Golden ratio** comes up in art, geometry, and  Greek mythology as a perfect ratio that is pleasing to the eye (and to the gods).

For instance, the rectangle shown below is said to have the dimensions of the Golden ratio, because the big rectangle has the same shape as the smaller rectangle inside. Mathematically, we have the ratios of lengths
$$ \frac{a+b}{a} = \frac{a}{b}.$$

![Golden ratio rectangle](images/Golden2.png)

Writing $x = \frac{a}{b}$, the above equation simplifies to
$$ 1 + \frac{1}{x} = x,$$
which is the same quadratic equation we saw for the limit of ratios of Fibonacci numbers.

For more information about the Golden ratio see
https://en.wikipedia.org/wiki/Golden_ratio

## A Formula for the Fibonacci Sequence $F_n$

Let's give the Golden ratio a special name. In honour of the ancient Greeks who used it so much, we call it `phi:'
$$ \varphi = \frac{1 + \sqrt{5}}{2}. $$
We'll call the other quadratic root 'psi:'
$$ \psi = \frac{1 - \sqrt{5}}{2}. $$
This number $\psi$ is called the **conjugate** of $\varphi$ because it looks the same, except for the negative sign in front of the $\sqrt{5}$.

Here's something **amazing.** It turns out that we have a remarkable formula for the Fibonnaci numbers, in terms of these two Greek numbers. The formula says
$$F_n = \frac{\varphi^n - \psi^n}{\sqrt{5}}.$$

#### Wow!

Seems amazing. And it is handy because now we can compute, say, the thousandth term in the sequence, $F_{1000}$ directly, without having to compute all the other terms that come before.

But, whenever someone gives you a formula, you should check it!

**Exercise 4:** Write a piece of code to show that the formula above, with $\varphi,\psi$ does produce, say, the first 20 Fibonnaci numbers.

**WAIT!** Don't go on until you try writing a program yourself, to compute the Fibonacci numbers using only powers of $\varphi, \psi$.

## SOLUTION (don't peak!)

from numpy import *  ## We need this to define square roots
phi = (1 + sqrt(5))/2
psi = (1 - sqrt(5))/2
for n in range(20):
    print( (phi**n - psi**n)/sqrt(5) )

Looking at that computer output, it does seem to give Fibonacci numbers, with a bit of numerical error.

## Checking the Math

Doing math, though, we like exact answers and we want to know why. So WHY does this formula $(\phi^n - \psi^n)/\sqrt{5}$ give Fibonacci numbers?

Well, we can check, step by step.

For $n=0$, the formula gives
$$\frac{\varphi^0 - \psi^0}{\sqrt{5}} = \frac{1-1}{\sqrt{5}} = 0,$$ which is $F[0]$, the first Fibonacci number.

For $n=1$, the formula gives
$$\frac{\varphi^1 - \psi^1}{\sqrt{5}} =
\frac{\frac{1 + \sqrt{5}}{2} - \frac{1 -\sqrt{5}}{2} }{\sqrt{5}} = \frac{\sqrt{5}}{\sqrt{5}} = 1,$$ which is $F[1]$, the next Fibonacci number.

For $n=2$, it looks harder because we get the squares $\varphi^2, \psi^2$ in the formula. But then remember that both $\varphi$ and $\psi$ solve the quadratic $x^2 = x+1$, so we know $\varphi^2 = \phi +1$ and $\psi^2 = \psi +1$. So we can write
$$\frac{\phi^2 - \psi^2}{\sqrt{5}} = \frac{\phi + 1 - \psi -1}{\sqrt{5}} = \frac{\phi - \psi }{\sqrt{5}} = 1,$$
since we already calculated this in the $n=1$ step. So this really is $F[2]=1$.

For $n=3,4,5,\ldots$ again it might seem like it will be hard because of the higher powers. But multiplying the formulas $\varphi^2 = \varphi +1$ and $\psi^2 = \psi +1$ by powers of $\phi$ and $\psi$, we get

$$\begin{eqnarray*}
\varphi^2 &=& \varphi +1,\quad \varphi^3 = \varphi^2+\varphi
,\quad \varphi^4=\varphi^3+\varphi^2,\qquad \dots \qquad %
\varphi^{n+2}=\varphi^{n+1}+{\varphi}^n,\quad \text{and} \\
\psi^2 &=&\psi +1,\quad \psi^3=\psi^2+\psi ,\quad \psi^4=\psi^3+\psi^2,\qquad
\dots \qquad \psi^{n+2}=\psi^{n+1}+\psi^n.
\end{eqnarray*}$$

So, assuming we know the generating formula already for $n$ and $n+1$ we can write the next term as
$$\frac{\varphi^{n+2} - \psi^{n+2}}{\sqrt{5}} = \frac{\varphi^{n+1} +\varphi^n - \psi^{n+1} - \psi^n}{\sqrt{5}}
= \frac{\varphi^{n+1} - \psi^{n+1}}{\sqrt{5}} + \frac{\varphi^{n} - \psi^{n}}{\sqrt{5}} = F[n+1] + F[n] = F[n+2].$$

So we do get $\frac{\varphi^{n+2} - \psi^{n+2}}{\sqrt{5}} = F[n+2]$, and the formula holds for all numbers n.

This method of verifying the formula for all n, based on previous values of n, is an example of **mathematical induction.**

## Why did this work?

Well, from the Golden ratio, we have the formula $\varphi^2 = \varphi + 1$, which then gives the formula $\varphi^{n+2} = \varphi^{n+1} + \varphi^n$.  This looks a lot like the Fibonacci formula $$F[n+2] = F[n+1] + F[n].$$ Same powers of $\psi$.

If we take ANY linear combination of powers of $\varphi, \psi$, such as
$$f(n) = 3\varphi^n + 4\psi^n,$$
we will get a sequence that behaves like the Fibonacci sequence, with $f(n+2) = f(n+1) + f(n).$ To get the 'right' Fibonacci sequence, we just have to replace the 3 and 4 with the right coefficients.

## From sequences to functions

Wouldn't it be fun to extend Fibonacci numbers to a function, defined for all numbers $x$?

The problems is the function
$$F[x] = \frac{\varphi^x - \psi^x}{\sqrt{5}}$$
is not defined for values of $x$ other than integers.

The issue is the term $\psi^{x}=\left( \frac{1-\sqrt{5}}{2}\right) ^{x}$, which is the power of a negative number.
We don't really know how to define that. For instance, what is the square root of a negative number?

To
overcome this technical difficulty, we write

$$\psi ^{x}=\left( -\left( -\psi \right) \right) ^{x}=\left( -\left( \frac{%
\sqrt{5}-1}{2}\right) \right) ^{x}=\left( -1\right) ^{x}\left( \frac{\sqrt{5}%
-1}{2}\right) ^{x}. $$

Now the factor $\left( \frac{\sqrt{5}-1}{2} \right) ^{x}$ make sense since
the number inside the brackets is positive. We have localized the problem into the powers of $-1$ for the term $\left(
-1\right) ^{x}$. We would like to replace this term  by a
continuous function $m(x)$ such that it takes the values $\pm1$ on the integers. That is,

$$m(n) =1\quad \text{if }n\text{ is even }\quad\text{and}\quad m(n) =-1\quad \text{if }n\text{ is odd.} $$

The cosine function works. That is

$$m\left( x\right) =\cos \left( \pi x\right) \qquad \text{does the job.} $$
That is:
$$\cos \left( n\pi \right) =1\quad \text{if }n\text{ is even}\quad\text{ and}\quad %
\cos \left( n\pi \right) =-1\quad \text{if }n\text{ is odd.}$$

Why this is a **good** choice would lead us to complex numbers and more!

Hence, we obtain the following closed formula for our function $F[x]:$

$$\begin{eqnarray*}
F[x]  &=&\frac{{\varphi }^{x}-\left( -1\right) ^{x}\left( -\psi
\right) ^{x}}{{\varphi -\psi }}=\frac{1}{\sqrt{5}}\left( {\varphi }%
^{x}-\left( -1\right) ^{x}\left( -\psi \right) ^{x}\right)  \\
&=&\frac{1}{\sqrt{5}}\left( \left( \frac{1+\sqrt{5}}{2}\right) ^{x}-\cos
\left( \pi x\right) \left( \frac{\sqrt{5}-1}{2}\right) ^{x}\right) .
\end{eqnarray*}$$

Let's plot this function, and the Fibonacci sequence.

## A plot of the continuous Fibonacci function

%matplotlib inline
from numpy import *
from matplotlib.pyplot import *

phi=(1+5**(1/2))/2
psi=(5**(1/2)-1)/2

x = arange(0,10)
y = (pow(phi,x) - cos(pi*x)*pow(psi,x))/sqrt(5)
xx = linspace(0,10)
yy = (pow(phi,xx) - cos(pi*xx)*pow(psi,xx))/sqrt(5)

figure(figsize=(10,6));
plot(x,y,'o',xx,yy);
title('The continuous Fibonacci function')
xlabel('$x$')
ylabel('$Fib(x)$');

## A plot with negative values

Well, with this general definition, we can even include negative numbers for $x$ in the function.

Let's plot this too.

%matplotlib inline
from numpy import *
from matplotlib.pyplot import *

phi=(1+5**(1/2))/2
psi=(5**(1/2)-1)/2

x = arange(-10,10)
y = (pow(phi,x) - cos(pi*x)*pow(psi,x))/sqrt(5)
xx = linspace(-10,10,200)
yy = (pow(phi,xx) - cos(pi*xx)*pow(psi,xx))/sqrt(5)

figure(figsize=(10,6));
plot(x,y,'o',xx,yy);
title('The Fibonacci function, extended to negative values')
xlabel('$x$')
ylabel('$Fib(x)$');

So we see we can even get negative Fibonacci numbers!

## The Golden Ratio and Continued Fractions

We have found that the Golden ratio ${\varphi =}\frac{{1+}\sqrt{5}}{2}$
satisfies the identity

$$
{\varphi =1+}\frac{1}{{\varphi }}.
$$

Substituting for ${\varphi }$ on the denominator in the right, we obtain

$$
{\varphi =1+}\frac{1}{{1+}\frac{1}{{\varphi }}}.
$$

Substituting again for ${\varphi }$ on the denominator in the right, we
obtain

$$
{\varphi =1+}\frac{1}{{1+}\dfrac{1}{{1+}\frac{1}{{\varphi }}}}.
$$

Repeating this again,

$$
{\varphi =1+}\frac{1}{{1+}\dfrac{1}{{1+}\dfrac{1}{{1+}\frac{1}{{\varphi }}}}}%
.$$

And again,

$$
{\varphi =1+}\frac{1}{{1+}\dfrac{1}{{1+}\dfrac{1}{{1+}\dfrac{1}{{1+}\frac{1}{%
{\varphi }}}}}}.
$$

And again,

$$
{\varphi =1+}\frac{1}{{1+}\dfrac{1}{{1+}\dfrac{1}{{1+}\dfrac{1}{{1+}\dfrac{1%
}{{1+}\frac{1}{{\varphi }}}}}}}.
$$

We see that this process can be $\textit{continued indefinitely}$. This results
in an $\textit{infinite expansion of a fraction}$. These type of expressions are known as
$\textbf{continued fractions}$:

$$
{\varphi =1+}\frac{1}{{1+}\dfrac{1}{{1+}\dfrac{1}{{1+}\dfrac{1}{{1+}\dfrac{1%
}{{1+}\dfrac{1}{{1+}\dfrac{1}{1+\dfrac{1}{{\vdots }}}}}}}}}.
$$

We can approximate continued fractions with the finite fractions obtained by
stopping the development at some point. In our case, we obtain the
approximates

$$
1,~1+1,~1+\frac{1}{1+1},~1+\frac{1}{1+\dfrac{1}{1+1}},~1+\frac{1}{1+\dfrac{1%
}{1+\dfrac{1}{1+1}}},~1+\frac{1}{1+\dfrac{1}{1+\dfrac{1}{1+\dfrac{1}{1+1}}}}%
,\dots
$$

Explicitly, these approximates are

$$
1,~2,~\frac{3}{2},~\frac{5}{3},~\frac{8}{5},~\frac{13}{8},\dots
$$

This looks like it is just the sequence of ratios $F_{n+1}/F_n$ we saw above!  How can we prove this is the case for all $n$?

We know that the sequence $R_{n} = F_{n+1}/F_n$ satisfies the recursive relation.

$$
R_{n}=\frac{F_{n+1}}{F_{n}}=1+\frac{F_{n-1}}{F_{n}}=1+\frac{1}{R_{n-1}}%
,\qquad \text{with}\qquad R_{1}=1.
$$

Then, we can generate all the terms in the sequence $R_{n}$ by staring with $%
R_{1}=1$, and then using the relation $R_{n+1}=1+\frac{1}{R_{n}}:$

$$
\begin{eqnarray*}
R_{1} &=&1 \\
R_{2} &=&1+\frac{1}{R_{1}}=1+\frac{1}{1}=2 \\
R_{3} &=&1+\frac{1}{R_{2}}=1+\frac{1}{1+R_{1}}=1+\frac{1}{1+1} \\
R_{4} &=&1+\frac{1}{R_{3}}=1+\frac{1}{1+\frac{1}{1+1}} \\
R_{5} &=&1+\frac{1}{R_{4}}=1+\frac{1}{1+\frac{1}{1+\frac{1}{1+1}}} \\
&&\vdots
\end{eqnarray*}
$$

This confirms that both the sequence of rations $R_{n}$ and the sequence of
approximations to the continuous fraction of ${\varphi }$ are the same
sequence. $\square $

In general, continued fractions are expressions of the form

$$
a_{0}+\frac{1}{a_{1}+\dfrac{1}{a_{2}+\dfrac{1}{a_{3}+\dots }}}
$$

where $a_{0}$ is an integer and $a_{1},a_{2},a_{3},\dots $ are positive
integers. These type of fractions are abbreviated by the notation

$$
\left[ a_{0};a_{1},a_{2},a_{3},\dots \right] =a_{0}+\frac{1}{a_{1}+\dfrac{1}{%
a_{2}+\dfrac{1}{a_{3}+\dots }}}.
$$

For example

$$
\begin{eqnarray*}
\left[ 1;1,1,2\right]  &=&1+\frac{1}{1+\dfrac{1}{1+\dfrac{1}{1+1}}}=\frac{8}{%
5} \\
&& \\
\left[ 1;1,1,1,1,\dots \right]  &=&{1+}\frac{1}{{1+}\dfrac{1}{{1+}\dfrac{1}{%
{1+}\dfrac{1}{{1+}\dfrac{1}{{1+}\dfrac{1}{{1+}\dfrac{1}{1+\dfrac{1}{{\vdots }%
}}}}}}}}={\varphi }
\end{eqnarray*}
$$

For more information of continued fractions, see
https://en.wikipedia.org/wiki/Continued_fraction

## Conclusion

### What have we learned?

- a **sequence** is an ordered list of numbers, which may go on forever.
- the **Fibonacci sequence** 0,1,1,2,3,5,8,13,... is a famous list of numbers, well-studied since antiquity.
- each number in this sequence is the sum of the two coming before it in the sequence.
- the sequence grows fast, increasing by a **factor** of about **10** for every **five** terms.
- the **ratio** of pairs of Fibonacci numbers converges to the **Golden ratio,** known since the ancient Greeks as the number
$$\varphi = \frac{1 + \sqrt{5}}{2} \approx 1.6108.$$
- the Fibonacci numbers can be computed directly as the difference of powers of $\varphi$ and its **conjugate,** $\psi = \frac{1 - \sqrt{5}}{2}.$ This is sometimes faster than computing the whole list of Fibonnaci numbers.
- this formula with powers of $\varphi, \psi$ is verified using **induction.**
- The Fibonacci numbers can be **extended** to a **continuous function** $Fib(x)$, defined for all real numbers $x$ (including negatives). It **oscillates** (wiggles) on the negative x-axis.
- The **Golden Ratio** can also be expressed a **continued fraction,** which is an infinite expansion of fractions with sub-fraction terms. Many interesting numbers come from interesting continued fraction forms.

[![Callysto.ca License](https://github.com/callysto/curriculum-notebooks/blob/master/callysto-notebook-banner-bottom.jpg?raw=true)](https://github.com/callysto/curriculum-notebooks/blob/master/LICENSE.md)


import numpy as np
import matplotlib.pyplot as plt

def computeCost(X, y, theta):

    #COMPUTECOST Compute cost for linear regression
    #   J = COMPUTECOST(X, y, theta) computes the cost of using theta as the
    #   parameter for linear regression to fit the data points in X and y

    # Initialize some useful values
    m = len(y) # number of training examples
    J = 0
	#	theta is an (n+1)-dimensional vector
	#	X is an m x (n+1)-dimensional matrix
	#	y is an m-dimensional vector
    s = (X.dot(theta) - np.transpose([y]))**2
    J = (1.0 / (2 * m)) * s.sum(axis=0)

    return J

def gradientDescent(X, y, theta, alpha, num_iters):

    # GRADIENTDESCENT Performs gradient descent to learn theta
    #   theta = GRADIENTDESENT(X, y, theta, alpha, num_iters) updates theta by
    #   taking num_iters gradient steps with learning rate alpha

    # Initialize some useful values
    m = len(y) # number of training examples
    J_history = np.zeros((num_iters, 1))
    for i in range(num_iters):
        theta = theta - alpha * (1.0 / m) * np.transpose(X).dot(X.dot(theta) - np.transpose([y]))
        J_history[i] = computeCost(X, y, theta)

    return theta

def plotData(x, y):
    # PLOTDATA Plots the data points x and y into a new figure
    #   PLOTDATA(x,y) plots the data points and gives the figure axes labels of
    #   population and profit.
    plt.plot(x, y, 'rx', markersize=10, label='Training data')
    plt.xlabel('Population of City in 10,000s')
    plt.ylabel('Profit in $10,000s')
    plt.show(block=False)  # prevents having to close the chart

def warmUpExercise(*args, **kwargs):
    return np.identity(5)

def featureNormalize(X):

    # FEATURENORMALIZE Normalizes the features in X
    #   FEATURENORMALIZE(X) returns a normalized version of X where
    #   the mean value of each feature is 0 and the standard deviation
    #   is 1. This is often a good preprocessing step to do when
    #   working with learning algorithms.
    X_norm = X
    mu = np.zeros((1, X.shape[1]))
    sigma = np.zeros((1, X.shape[1]))
    for i in range(X.shape[1]):
    	mu[:,i] = np.mean(X[:,i])
    	sigma[:,i] = np.std(X[:,i])
    	X_norm[:,i] = (X[:,i] - float(mu[:,i]))/float(sigma[:,i])

    return X_norm, mu, sigma

def gradientDescentMulti(X, y, theta, alpha, num_iters):

    # GRADIENTDESCENTMULTI Performs gradient descent to learn theta
    #   theta = GRADIENTDESCENTMULTI(x, y, theta, alpha, num_iters) updates theta by
    #   taking num_iters gradient steps with learning rate alpha

    # Initialize some useful values
    m = len(y) # number of training examples
    J_history = np.zeros((num_iters, 1))

    for i in range(num_iters):
        theta = theta - alpha*(1.0 / m) * np.transpose(X).dot(X.dot(theta) - np.transpose([y]))
        J_history[i] = computeCost(X, y, theta)

    return theta, J_history

def normalEqn(X, y):
    #   NORMALEQN(X,y) computes the closed-form solution to linear
    #   regression using the normal equations.
    theta = np.zeros((X.shape[1], 1))
    theta = np.linalg.pinv(np.transpose(X).dot(X)).dot(np.transpose(X).dot(y))

    return theta


from sympy import (sympify, factorial, var, cos, S, sin, Dummy, sqrt, pi, exp,
        I, latex, symbols)

def Plm(l, m, z):
    """
    Returns the associated Legendre polynomial P_{lm}(z).

    The Condon & Shortley (-1)^m factor is included.
    """
    l = sympify(l)
    m = sympify(m)
    z = sympify(z)
    if m >= 0:
        r = ((z**2-1)**l).diff(z, l+m)
        return (-1)**m * (1-z**2)**(m/2) * r / (2**l * factorial(l))
    else:
        m = -m
        r = ((z**2-1)**l).diff(z, l+m)
        return factorial(l-m)/factorial(l+m) * (1-z**2)**(m/2) * r / (2**l * factorial(l))

def Plm_cos(l, m, theta):
    """
    Returns the associated Legendre polynomial P_{lm}(cos(theta)).

    The Condon & Shortley (-1)^m factor is included.
    """
    l = sympify(l)
    m = sympify(m)
    theta = sympify(theta)
    z = Dummy("z")
    r = ((z**2-1)**l).diff(z, l+m).subs(z**2-1, -sin(theta)**2).subs(z, cos(theta))
    return (-1)**m * sin(theta)**m * r / (2**l * factorial(l))

def Ylm(l, m, theta, phi):
    """
    Returns the spherical harmonics Y_{lm}(theta, phi) using the Condon & Shortley convention.
    """
    l, m, theta, phi = sympify(l), sympify(m), sympify(theta), sympify(phi)
    return sqrt((2*l+1)/(4*pi) * factorial(l-m)/factorial(l+m)) * Plm_cos(l, m, theta) * exp(I*m*phi)

def Zlm(l, m, theta, phi):
    """
    Returns the real spherical harmonics Z_{lm}(theta, phi).
    """
    l, m, theta, phi = sympify(l), sympify(m), sympify(theta), sympify(phi)
    if m > 0:
        return sqrt((2*l+1)/(2*pi) * factorial(l-m)/factorial(l+m)) * Plm_cos(l, m, theta) * cos(m*phi)
    elif m < 0:
        m = -m
        return sqrt((2*l+1)/(2*pi) * factorial(l-m)/factorial(l+m)) * Plm_cos(l, m, theta) * sin(m*phi)
    elif m == 0:
        return sqrt((2*l+1)/(4*pi)) * Plm_cos(l, 0, theta)
    else:
        raise ValueError("Invalid m.")

def Zlm_xyz(l, m, x, y, z):
    """
    Returns the real spherical harmonics Z_{lm}(x, y, z).

    It is assumed x**2 + y**2 + z**2 == 1.
    """
    l, m, x, y, z = sympify(l), sympify(m), sympify(x), sympify(y), sympify(z)
    if m > 0:
        r = (x+I*y)**m
        r = r.as_real_imag()[0]
        return sqrt((2*l+1)/(2*pi) * factorial(l-m)/factorial(l+m)) * Plm(l, m, z) * r / sqrt(1-z**2)**m
    elif m < 0:
        m = -m
        r = (x+I*y)**m
        r = r.as_real_imag()[1]
        return sqrt((2*l+1)/(2*pi) * factorial(l-m)/factorial(l+m)) * Plm(l, m, z) * r / sqrt(1-z**2)**m
    elif m == 0:
        return sqrt((2*l+1)/(4*pi)) * Plm(l, 0, z)
    else:
        raise ValueError("Invalid m.")

var("theta phi")
x, y, z = symbols("x y z", real=True)
print "Spherical harmonics:"
print
print ".. math::"
print
for l in range(4):
    for m in range(-l, l+1):
        print r"    Y_{%d,%d}(\theta, \phi) =" % (l, m), \
            latex(Ylm(l, m, theta, phi))
        print

print
print "Real spherical harmonics:"
print
print ".. math::"
print
for l in range(4):
    for m in range(-l, l+1):
        print r"    Z_{%d,%d}(\theta, \phi) =" % (l, m), \
            latex(Zlm(l, m, theta, phi))
        print

print
print "Real spherical harmonics (using $x$, $y$ and $z$, assuming $x^2 + y^2 + z^2 = 1$):"
print
print ".. math::"
print
for l in range(4):
    for m in range(-l, l+1):
        print r"    Z_{%d,%d}(x, y, z) =" % (l, m), \
            latex(Zlm_xyz(l, m, x, y, z).simplify())
        print


########################################
# Overview
#
# This file defines loss functions for
# generalized linear models. These functions
# are the negative log likelihoods using the
# canonical links.
########################################
import numpy as np
from scipy.special import factorial, gamma

def SSE(Y, X, B):
    Y_Hat = np.matmul(X, B)
    Y_Hat.reshape([Y_Hat.shape[0],1])
    loss = np.sum( np.power(Y - Y_Hat, 2))

    return loss

########################################
# Mathematical background
# Step 1 Calculate eta
# Step 2 Inverse link
# step 3 Calculate negative log likelihood
########################################

def neg_ll_gaussian(Y, X, B):
    Y_Hat = np.matmul(X, B)
    Y_Hat.reshape([Y_Hat.shape[0],1])
    Y_Hat = Y_Hat # Inverse link

    n = X.shape[0]
    p = X.shape[1]
    sigma = np.sum(np.power(Y - Y_Hat, 2)) / (n-p)

    ll = -(n/2)*np.log(2*np.pi) - n*np.log(sigma) - (1/2*np.power(sigma, 2)) * np.sum(np.power(Y-Y_Hat,2))
    ll = -1 * ll

    return ll

def neg_ll_poisson(Y, X, B):
    Y_Hat = np.matmul(X, B)
    Y_Hat.reshape([Y_Hat.shape[0],1])
    Y_Hat = np.exp(Y_Hat) # Inverse link

    ll = Y * np.log(Y_Hat) - Y_Hat - np.log(factorial(Y))
    ll = np.sum(ll)
    ll = -1 * ll

    return ll

def neg_ll_bernoulli(Y, X, B):
    Y_Hat = np.matmul(X, B)
    Y_Hat.reshape([Y_Hat.shape[0],1])
    Y_Hat = np.exp(Y_Hat) / (1 + np.exp(Y_Hat)) # Inverse link

    ll = Y * np.log(Y_Hat) + (1 - Y) * np.log(1 - Y_Hat)
    ll = np.sum(ll)
    ll = -1 * ll

    return ll

def neg_ll_gamma(Y, X, B):
    Y_Hat = np.matmul(X, B)
    Y_Hat.reshape([Y_Hat.shape[0],1])
    Y_Hat = np.power(Y_Hat, -1) # Inverse link

    # Using method of moments estimate
    # MLE does not have closed form.
    # Don't want to write numerical method for MLE
    n = X.shape[0]
    p = X.shape[1]
    numerator = np.power(Y - Y_Hat, 2)
    denominator = np.power(Y_Hat, 2) * (n-p)
    phi = np.sum(numerator / denominator)

    ll = -1 * np.log(Y) - np.log(gamma(1/phi)) + (1/phi) * (np.log(Y) - np.log(Y_Hat) - np.log(phi)) - Y/(Y_Hat * phi)
    ll = np.sum(ll)
    ll = -1 * ll

    return ll


#--------#--------#--------#--------#--------#--------#--------#--------#--------#--------#
#   QR Algorithm for obtaining eigenvalues and eigenvectors from tridiagonal matrices     #
#                                      20/07/21                                           #
#--------#--------#--------#--------#--------#--------#--------#--------#--------#--------#

import numpy as np
import math
from functools import reduce

#Generates a rotation matrix
def generateRotationMatrix(shape, c_mat, s_mat, i_mat, j_mat):
    matrixG = np.array(np.zeros((shape,shape)))
    for i in range(0, shape):
        for j in range(0, shape):
            if (i == j) and (i == i_mat or j == j_mat): #Gkk = c for k == i,j
                matrixG[i,j] = c_mat
            elif (i == j) and (i != i_mat or j != j_mat): #Gkk = 1 for k != i,j
                matrixG[i,j] = 1
    matrixG[i_mat,j_mat] = s_mat
    matrixG[j_mat,i_mat] = -s_mat
    return matrixG

#Triangularization function using rotation matrix
def givensRotation(matrixA):
    QRResults = []
    GMatrices = []
    dimension = matrixA.shape[0]
    matrixAk = matrixA
    for i in range(1, dimension):
        if matrixAk[i,i-1] != 0:
            if abs(matrixAk[i-1,i-1]) > abs(matrixAk[i,i-1]): #Numerically stable
                tau = (-matrixAk[i,i-1])/matrixAk[i-1,i-1]
                c = 1/(math.sqrt(1+pow(tau,2)))
                s = c*tau
            else:
                tau = (-matrixAk[i-1,i-1])/matrixAk[i,i-1]
                s = 1/(math.sqrt(1+pow(tau,2)))
                c = s*tau
            G = generateRotationMatrix(dimension, c, s, i, i-1)
            GMatrices.append(G)
            matrixAk = np.matmul(G, matrixAk)
    matrixR = matrixAk #Upper triangular
    GMatricesTransp = [np.transpose(GMatrices[i]) for i in range(0, len(GMatrices))]
    matrixQ = reduce(np.matmul, GMatricesTransp) #np.matmul of all transposed G matrices
    QRResults = [matrixQ, matrixR]
    return QRResults

#QR Algorithm
def QRAlgorithm(A, desloc):
    Ak = A
    shape = A.shape[0] #Matrix A, k = 0
    Vk = np.identity(shape) #Matrix V, k = 0, V0 = I
    errval = 10e-6
    k = 0 #Number of iterations
    for m in range(1, shape):
        while abs(Ak[m,m-1]) > errval:
            if k == 0 or desloc == False:
                mi = 0
            else:
                dk = (Ak[m-1,m-1] - Ak[m,m])/2 #dk = (alpha_n-1 + alpha_n)/2
                if dk >= 0:
                    sign = 1
                else:
                    sign = -1
                mi = (Ak[m,m] + dk - (sign*np.sqrt(pow(dk,2)+pow(Ak[m,m-1],2))))
            QRArray = givensRotation((Ak - (np.identity(shape))*mi)) #QR Factorization
            Qk = QRArray[0]
            Rk = QRArray[1]
            Ak = (np.matmul(Rk,Qk) + (np.identity(shape))*mi) #Updates the matrix
            Vk = np.matmul(Vk,Qk) #Updates eigenvectors
            k+=1
    print("->" + str(k) + " Iteracoes do Algoritmo QR")
    results = [Ak,Vk]
    return results


import numpy as np

def inverse_iteration(A, x, shift=2):
	diff = 10000
	diffn = 0
	xn = None
	xo = x
	lamda = 0
	iters = 0
	A = A - shift * np.diag(np.diag(np.ones(A.shape)))
	while(diff > 10**(-12)):
		iters+=1
		yn = np.linalg.solve(A, xo)
		xn = yn / np.linalg.norm(yn, ord=2)
		lamda = np.linalg.norm(yn, ord=2)
		diff = np.linalg.norm((xn - xo), ord=2) / np.linalg.norm(xo, ord=2)
		xo = xn

	return 1/(lamda) + shift, xn, iters

if __name__ == '__main__':
	print("Inverse Iteration")
	print("-------------------\n\n")

	seeds = [1, 89, 98, 23, 88, 91, 101, 11, 17, 19]

	for i in range(10):
		A = np.array([[6, 2, 1], [2, 3, 1], [1, 1, 1]])
		np.random.seed(seeds[i])
		print('Seed:', seeds[i])
		x0 = np.random.random((3,))
		l, x, iters = inverse_iteration(A, x0)
		print("Computed Eigenvalue:", l)
		print("Computed Eigenvector:\n\t", x, "'\n")
		print("Number of iterations:", iters)
		print("\n\n")


# for activation functions of Neural Networks
import numpy as np

# activation function
def linear(x):
    """
    Performs linear function on the input elements.
    Linear function: f(x) = x

    :param x: {array}, shape {n_samples,}
            input to the function.
    :return: {array}, shape {n_samples,}
            output of linear function.
    """
    return x

def logistic(x):
    """
    Performs logistic function on the input elements.
    Logistic function: f(x) = 1/(1+exp(-x))

    :param x: {array}, shape {n_samples,}
            input to the function.
    :return: {array}, shape {n_samples,}
            output of logistic function.
    """
    return 1 / (1 + np.exp(-x))

def relu(x):
    """
    Performs Rectified Linear Unit function on the input elements.
    Rectified Linear Unit: f(x) = max(0,x)

    :param x: {array}, shape {n_samples,}
            input to the function.
    :return: {array}, shape {n_samples,}
            output of relu function.
    """
    return np.maximum(x, 0)

def tanh(x):
    """
    Performs Bipolar logistic function on the input elements.
    Bipolar logistic function: f(x) = tanh(x) = (exp(x)-exp(-x))/(exp(x)+exp(-x))
                                    = 2*(1/(1+exp(-2*x)) - 1
    :param x: {array}, shape {n_samples,}
            input to the function.
    :return: {array}, shape {n_samples,}
            output of tanh function.
    """
    return 2 * logistic(x) - np.ones(x.shape)

def rbf(x):
    """
    Performs Radial Basis function on the input elements.
    Radial Basis function: f(x) = exp(-(x - mean_x)**2/(2*var_x)) / sqrt(2*pi*var_x)

    :param x: {array}, shape {n_samples,}
            input to the function.
    :return: {array}, shape {n_samples,}
            output of rbf function.
    """
    mean_x = np.mean(x)
    var_x = np.var(x)
    z = (x - mean_x)
    return np.exp(-z**2 / (2 * var_x)) / np.sqrt(2 * np.pi * var_x)

activations = {"logistic": logistic, "relu": relu,
               "tanh": tanh, "rbf": rbf, "linear": linear}


"""
Last change: 30.07.2018
"""

# Some imports
import numpy as np
import math
from scipy import stats

class Standardization:

    def __init__(self):
        pass

    def standardize(self, x):

        """
        Standardization: x_std = (x - x_mean) / std_dev

        Inputs:
        - x: input vector (has to be a row vector)

        Returns:
        - x_norm: standardized vector of the inputed vector
        """

        mean = self.compute_mean(x)
        std_dev = self.compute_standard_deviation(x)

        x_norm = np.zeros_like(x)
        for i in range(x.shape[1]):

            x_norm[0, i] = (x[0, i] - mean) / std_dev

        return x_norm

    def compute_mean(self, x):

        """
         Computes mean of the inputed vector.

        Inputs:
         - x: input vector (has to be a row vector)

        Returns:
        - mean: mean value of the inputed vector
        """

        sm = np.sum(x)
        ln = x.shape[1]

        mean = sm / ln

        return mean

    def compute_standard_deviation(self, x):

        """
        Computes standard deviation of the inputed vector.

        Inputs:
        - x: input vector (has to be a row vector)

        Returns:
        - std_dev: standard deviation of the inputed vector
        """

        sm = np.sum(x)
        mean = self.compute_mean(x)

        summ = 0
        for i in range(x.shape[1]):
            summ = summ + (x[0, i] - mean)**2

        std_dev = math.sqrt(summ/x.shape[1])

        return std_dev

"""
# Test

# Instantiate class
obj = Normalization()

# Generate random vector
np.random.seed(5)
v = 10*np.random.random([1, 10])
print('Vector: ', v)

# Compute mean
mean = np.mean(v)
print('Mean: ', mean)

# Compute manual mean
mean_manual = obj.compute_mean(v)
print('Mean manual', mean_manual)

# Compute standard deviation
std_dev = np.std(v)
print('Standard deviation: ', std_dev)

# Compute manual standard deviation
std_dev_manual = obj.compute_standard_deviation(v)
print('Standard deviation manual', std_dev_manual)

# Standardize
standard = stats.zscore(v, axis=1)
print('Standardized: ', standard)

# Manual standardized
standard_manual = obj.standardize(v)
print('Standardized manual: ', standard_manual)
"""


'''
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Taking user input ~~~~~~~~~~~~~~~~~~~~~~~~~~~
while True:
    try:
        N = int(input("Enter the Number of Points: "))
        if N > 1: break
        else: print("Number of Points must be greater than 1")
    except ValueError:
        print("Enter a Natural Number greater than 1")
X, Y = [], []
for i in range(N):
    while True:
        try:
            x = float(input("Enter the X coordinate of the point "+str(i+1)+": "))
        except ValueError:
                print("Enter a Real Number")
                continue
        else: break
    X.append(x)
    while True:
        try:
            y = float(input("Enter the Y coordinate of the point "+str(i+1)+": "))
        except ValueError:
                print("Enter a Real Number")
                continue
        else: break
    Y.append(y)
print()
'''

# X = [0, 1, 2, 3, 4, 5]
# Y = [2.1, 7.7, 13.6, 27.2, 40.9, 61.1]

X = [3, 4, 5, 6, 7, 8]
Y = [0, 7, 17, 26, 35, 45]

N = len(X)  # Number of data points

# ~~~~~~~~~~~~~~~~~~~~~~~~ Method 1: Using for loop ~~~~~~~~~~~~~~~~~~~~~~~~

sumx = sumy = sumx2 = sumxy = 0 # Initial value of summation variables
for i in range(N):
    sumx += X[i]
    sumy += Y[i]
    sumx2 += X[i]**2
    sumxy += X[i]*Y[i]
meanx = sumx / N
meany = sumy / N

a1 = (N*sumxy - sumx*sumy) / (N*sumx2 - sumx**2)
a0 = meany - a1 * meanx

# ~~~~~~~~~~~~~~~~~ Method 2: Using numpy array, sum, mean ~~~~~~~~~~~~~~~~~

from numpy import array, mean

# sum(X**2) requires X to be a numpy array as python list cannot be squared
X = array(X, float)

# a1 = (sum(X*Y) - mean(X)*sum(Y))/(sum(X**2) - N*mean(X)**2)
# a0 = (mean(Y)*sum(X**2) - mean(X)*sum(X*Y))/(sum(X**2) - N*mean(X)**2)

a1 = (N*sum(X*Y) - sum(X)*sum(Y)) / (N*sum(X**2) - sum(X)**2)
a0 = mean(Y) - a1 * mean(X)

print("The straight line equation :")
s = "-" if a1<0 else "+"
a1 = -a1 if a1<0 else a1
print('y = %.3f %s %.3f x' %(a0, s, a1))

'''
y = a0 + a1 x + e    # where a0 = intercept, a1 = slope, y = mx+c
e = y - a0 - a1 x    # e = true value of y ~ approximate value of a0+a1x

Criteria for a best fit:
∑ (ei) = ∑ (yi - a0 - a1 xi)

Sr = ∑ (ei)^2 = ∑ (yi - a0 - a1 xi)^2

∂Sr/∂a0 = -2    ∑ (yi - a0 - a1 xi)
∂Sr/∂a1 = -2 xi ∑ (yi - a0 - a1 xi)

0 = ∑    yi - ∑ a0    - ∑ a1 xi
0 = ∑ xi yi - ∑ a0 xi - ∑ a1 xi^2

(∑ xi) a0 + (∑ xi^2) a1 = ∑ xi yi
     n a0 + (∑ xi  ) a1 = ∑    yi    # ∑ a0 = n a0

Cross Multiplication:
a1 = ( n ∑ xi yi - ∑ xi ∑ yi ) / ( n ∑ xi^2 - (∑ xi)^2 )

a0 = ( ∑ yi - (∑ xi) a1 ) / n
   = mean(y) - a1 mean(x)

or,
a1 = ( n ∑ xi yi - ∑ xi ∑ yi )/n / ( n ∑ xi^2 - (∑ xi)^2 )/n
   = ( ∑ xi yi - mean(x) ∑ yi ) / ( ∑ xi^2 - n mean(x)^2 )
a0 = ( ∑ xi^2 ∑ yi - ∑ xi ∑ xi yi )/n / ( n ∑ xi^2 - (∑ xi)^2 )/n
   = ( mean(y) ∑ xi^2 - mean(x) ∑ xi yi ) / ( ∑ xi^2 - n mean(x)^2 )

'''

2. Interpolation and Curve Fitting/5. Linear (Least Squares) Regression or Fit.py


import numpy as np

def l2_norm(x, y=None):
    if y is None:
        return np.linalg.norm(x)
    else:
        return np.linalg.norm(x - y)

def mean(matrix, axis=0):
    return np.average(matrix, axis=axis)

def get_centroid(matrix, centroid_method=mean):
    return centroid_method(matrix)

def get_distance(x, y, distance_metric=l2_norm):
    return distance_metric(x, y)

def rbf_kernel(x, y, sigma=1, norm=l2_norm):
    return np.exp(-(norm(x, y) ** 2) / (2 * (sigma ** 2)))

def kernel_matrix(x, y, kernel=rbf_kernel, tuner=1):
    m = x.shape[1]
    n = y.shape[1]
    mat = np.zeros((m, n))
    for i in range(m):
        for j in range(n):
            mat[i, j] = kernel(x[:, i], y[:, j], tuner)
    return mat


import math
import numpy as np

def cholensky(__A):
    _A = np.array(__A)
    # return np.linalg.cholesky(_A)
    n = list(np.shape(_A))[0]
    A = np.zeros([n, n], dtype='double')
    for i in range(n):
        for j in range(i+1):
            A[i, j] = _A[i, j]
    for j in range(n):
        A[j, j] = np.sqrt(A[j, j] - np.linalg.norm(A[j, 0:j])**2)
        for i in range(j+1, n):
            tmp = 0
            for k in range(j):
                tmp += A[i, k] * A[j, k]
            A[i, j] = (A[i, j] - tmp) / A[j, j]

    return A

def cho_solve(A, _b):
    n = list(np.shape(A))[0]
    b = np.array(_b)
    L = cholensky(A)
    Lt = np.transpose(L)

    y = np.zeros(n, dtype='double')
    for i in range(n):
        for j in range(n - 1 - i):
            b[i + j + 1] -= b[i] * L[i + j + 1, i] / L[i, i]
        y[i] = b[i] / L[i, i]

    x = np.zeros(n, dtype='double')
    for i in range(n):
        for j in range(n - 1 - i):
            y[n - 1 - (i + j + 1)] -= y[n-1 - i] * Lt[n - 1 - (i + j + 1), n - 1 - i] / Lt[n - 1 - i, n - 1 - i]
        x[n - 1 - i] = y[n - 1 - i] / Lt[n - 1 - i, n - 1 - i]
    return x

def hilbert(n):
    ret = np.zeros([n, n])
    for i in range(n):
        for j in range(n):
            ret[i][j] = 1.0 / (i + j + 1)

    return ret

def my_sum(cb, max_count, dt):
    sigma = dt(0.0)
    n = dt(1.0)
    while True:
        sigma1 = sigma + dt(1) / dt(n)
        cb(n, sigma)
        if sigma1 == sigma:
            break
        if n == max_count:
            break
        n += dt(1)
        sigma = sigma1
    return sigma

def newton(f, df, x0, get_lambda, lambda0, epsilon, cb):
    k = 0
    x = x0
    while True:
        s = f(x) / df(x)
        x1 = x - s
        i = 0
        y = f(x)
        la = 1
        while True:
            y1 = f(x1)
            if abs(y1) < abs(y):
                break
            la = get_lambda(lambda0, i)
            x1 = x - la * s
            i += 1
        cb(k, la, y, y1, x, x1)
        k += 1
        if abs(x1 - x) <= epsilon:
            break
        x = x1

def iteration_solver(A, b, x0, threshold, cb, method='jacobi', omega=1):
    x = x0
    iter_count = 0
    n = list(np.shape(b))[0]
    if method == 'jacobi':
        while True:
            y = np.zeros(n, dtype='double')
            for i in range(n):
                a = np.dot(A[i], x) - A[i, i] * x[i]
                new_xi = (b[i] - a) / A[i, i]
                y[i] = new_xi
            delta = np.linalg.norm(y - x)
            if delta <= threshold:
                break
            if delta > 1e10:
                break
            cb(iter_count, x, y - x)
            x = y
            iter_count += 1
        print('Total iter %d, delta %g' % (iter_count, delta))
        return x
    else:
        while True:
            y = np.array(x)
            for i in range(n):
                a = np.dot(A[i], x) - A[i, i] * x[i]
                new_xi = (1 - omega) * x[i] + omega * (b[i] - a) / A[i, i]
                x[i] = new_xi
            delta = np.linalg.norm(y - x)
            if delta <= threshold:
                break
            cb(iter_count, x, y - x)
            iter_count += 1
        print('Total iter %d, delta %g' % (iter_count, delta))
        return x

def power(A, x0, threshold, cb):
    u = x0
    last_lambda1 = 0
    i = 0
    while True:
        v = np.dot(A, u)
        lambda1 = np.max(v)
        u = v / lambda1
        delta = abs(last_lambda1 - lambda1)
        if delta < threshold:
            break
        cb(i, u, lambda1, delta)
        last_lambda1 = lambda1
        i += 1
    return lambda1, u

def fitting(pts, n):
    phi = []
    for i in range(n):
        phi.append(np.power(pts[0], i))

    A = np.zeros((n, n), dtype='double')
    for i in range(n):
        for j in range(n):
            A[i, j] = np.dot(phi[i], phi[j])

    b = np.zeros(n, dtype='double')
    for i in range(n):
        b[i] = np.dot(phi[i], pts[1])

    x = cho_solve(A, b)

    return x
    #  tt = 0
    # For each point
    #  for _ in range(list(np.shape(pts))[1]):
    #      A = np.zeros((n, n), dtype='double')
    #      for i in range(n):
    #          for j in range(n):
    #              A[i, j] = pts[0][i] * pts[0][j]
    #      tt += np.linalg.norm(np.dot(A, x) - b) ** 2
    #  d = np.sqrt(tt / list(np.shape(pts))[1])
    #  return x, d


import numpy as np
import matplotlib.pyplot as plt

def normal_pdf(x, mu, sigma):
    pdf = 1/(sigma * np.sqrt(2 * np.pi)) * np.exp( - (x - mu)**2 / (2 * sigma**2))
    return pdf

fig = plt.figure(figsize=(6,9))

# Random sampling of a normal distribution
my_mu, my_sigma = 0, 0.1 # mean and standard deviation

bit_generators = [np.random.MT19937(), np.random.Philox(), np.random.SFC64()]
names = ['Mersenne Twister PRNG (MT19937)', 'Philox (4x64) PRNG (Philox)', 'Chris Doty-Humphrey\'s SFC PRNG (SFC64)']
indexes = [1,2,3]

for bit_generator, name, index in zip(bit_generators, names, indexes):
    sn = np.random.Generator(bit_generator).normal(loc = my_mu, scale = my_sigma, size = 10000)
    ax = fig.add_subplot(3, 1, index)
    ax.hist(sn, density=True, bins='auto', edgecolor='k', color='#c7ddf4', label=name)
    my_xn = np.linspace(my_mu - 4 * my_sigma, my_mu + 4 * my_sigma, 1000)
    my_yn = normal_pdf(x=my_xn, mu=my_mu, sigma=my_sigma)
    ax.plot(my_xn, my_yn, linewidth=2, linestyle='--', color='#ff464a', label ='Target Normal PDF')
    ax.set_ylim(0.0, 7.0)
    ax.set_xlim(my_mu - 6 * my_sigma, my_mu + 6 * my_sigma)
    ax.set_xlabel('x')
    ax.set_ylabel('Probability Density')
    ax.legend()

fig.tight_layout()


def approx_first_derivative_no_main_block(f,x,h):
    """
    Numerical differentiation by finite differences. Uses central point formula
    to approximate first derivative of function.
    Args:
        f (function): function definition.
        x (float): point where first derivative will be approximated
        h (float): step size for central differences. Tipically less than 1
    Returns:
        df (float): approximation to first_derivative.
    """
    df = (f(x+h) - f(x-h))/(2.0*h)
    return df

def approx_second_derivative_no_main_block(f,x,h):
    """
    Numerical differentiation by finite differences. Uses central point formula
    to approximate second derivative of function.
    Args:
        f (function): function definition.
        x (float): point where second derivative will be approximated
        h (float): step size for central differences. Tipically less than 1
    Returns:
        ddf (float): approximation to second_derivative.
    """
    ddf =(f(x+h) - 2.0*f(x) + f(x-h))/h**2
    return ddf

## Python libraries
import numpy as np

## Parameters
f = np.arctan
x = 0.9
h = 1e-6

## Main code
res_first_d = approx_first_derivative_no_main_block(f,x,h)
res_second_d = approx_second_derivative_no_main_block(f,x,h)

print(res_first_d)
print(res_second_d)


from numpy.random import rand
from math import sqrt
from sys import argv

def distance(array1, array2):
	assert len(array1) == len(array2), "Arrays must be of the same dimension"
	return sqrt(sum([(array2[i] - array1[i])**2 for i in range(len(array1))]))

if __name__ == '__main__':
	dim = int(argv[1])
	assert dim >= 2, "Dimension must be greater than or equal to two"
	iterations = int(argv[2]) if len(argv) >= 3 else 10000
	assert iterations >= 10, "Iterations must be greater than or equal to 10"
	sum_dist = 0
	for i in range(iterations):
		array1 = rand(dim)
		array2 = rand(dim)
		sum_dist += distance(array1, array2)
	estimate = round(sum_dist / iterations, 5)
	print(f"With {iterations} iterations, the estimated distance between two points in a(n) {dim}-dimensional cube is {estimate}")


import math
import numpy as np
from PIL import Image

def basic_sigmoid(x):
    s = 1/(1+math.exp(-x))
    return s
print(1, basic_sigmoid(10))

x = np.array([1,2,3])
print(2, x+3)

def sigmoid(x):
    s = 1/(1+np.exp(-x))
    return s
print(3, sigmoid(x))

def sigmoid_derivative(x):
    s = 1/(1+np.exp(-x))
    ds = s*(1-s)
    return ds
print(4, sigmoid_derivative(x))

def image2vector(image):
    v = image.reshape((image.shape[0] * image.shape[1] * image.shape[2]), 1)
    return v
image = np.array(Image.open("./icon_sample.JPG"))
print(5, image2vector(image))

def normalizeRows(x):
    x_norm = np.linalg.norm(x, axis=1, keepdims=True)
    x = x/x_norm
    return x
x = np.array([
    [0,3,4], [1,6,4]
])
print(6, normalizeRows(x))

def softmax(x):
    x_exp = np.exp(x)
    x_sum = np.sum(x_exp, axis=1, keepdims=True)
    s = x_exp/x_sum
    return s
x = np.array([
    [1,2,3],
    [4,5,6],
    [7,8,9]
])
print(7, softmax(x))


# -*- coding: utf-8 -*-
#
import math

import scipy.special
import sympy

def integrate_monomial_over_unit_simplex(k, symbolic=False):
    """The integrals of monomials over the standard triangle and tetrahedron are
    given by

    \\int_T x_0^k0 * x1^k1 = (k0!*k1!) / (2+k0+k1)!,
    \\int_T x_0^k0 * x1^k1 * x2^k2 = (k0!*k1!*k2!) / (4+k0+k1+k2)!,

    see, e.g.,
    A set of symmetric quadrature rules on triangles and tetrahedra,
    Linbo Zhang, Tao Cui and Hui Liu,
    Journal of Computational Mathematics,
    Vol. 27, No. 1 (January 2009), pp. 89-96,
    <https://www.jstor.org/stable/43693493>.

    See, e.g., <https://math.stackexchange.com/q/207073/36678> for a formula in
    all dimensions.
    """
    if symbolic:
        return sympy.prod([sympy.gamma(kk + 1) for kk in k]) / sympy.gamma(
            sum(k) + len(k) + 1
        )
    # exp-log to account for large values in numerator and denominator
    # import scipy.special
    return math.exp(
        math.fsum([scipy.special.gammaln(kk + 1) for kk in k])
        - scipy.special.gammaln(sum([kk + 1 for kk in k]) + 1)
    )


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

def lagranz_intrepolation(X,Y,x):
   z =  0
   for j in range(len(Y)):
       p1 = 1
       p2 = 1
       for i in range(len(X)):
           if i == j :
             continue
           else:
             p1 = p1 * (x - X[i])
             p2 = p2 * (X[j] - X[i])
       z = z + Y[j]/p2*p1
   return z

def newton_interpolation(x, y, u):
    g = y[:]
    s = g[0]
    for i in range(len(y) - 1):
        g = [(g[j + 1] - g[j]) / (x[j + i + 1] - x[j]) for j in range(len(g) - 1)]
        s += g[0] * product(u - x[j] for j in range(i + 1))
    return s

def product(a):
    p = 1
    for i in a: p *= i
    return p

def main():
    X = np.array([0,np.pi/6,np.pi/4,np.pi/3,np.pi/2])
    Y = 4*np.sin(X)**2
    x_in = np.sin(np.pi/7)
    y_true = 4*np.sin(x_in)**2
    xnew = np.linspace(np.min(X),np.max(X),100)
    Y_lagranz = np.array([lagranz_intrepolation(X,Y,i) for i in xnew])
    Y_newton = np.array([newton_interpolation(X,Y,i) for i in xnew])
    y_newtwon = newton_interpolation(X,Y,x_in)
    y_lagranz = lagranz_intrepolation(X,Y,x_in)
    print("In newtwon : " + str(y_true-y_newtwon))
    print("In lagranz :" + str(y_true-y_lagranz))
    plt.plot(X,Y,'o')
    plt.plot(xnew,Y_lagranz,'g')
    plt.plot(xnew,Y_newton,'b')
    plt.grid(True)
    pylab.show()
main()


# Z-Test Assignement

"""
A sample of 400 male students is found to have a mean height of 171.38cms. Can it be reasonably regarded as a sample from a large population with mean height 171.17 and standard deviation 3.30cms
"""
print("Assignment 1")
import numpy as np
n=400
sMean=171.38
pMean=171.17
pStd=3.30

zstatistics=(sMean - pMean)/(pStd/np.sqrt(n))
print("Zstatistics :",zstatistics)

from scipy.stats import norm

zcritical_l=norm.ppf(q=0.05/2)
zcritical_u=-zcritical_l
print("Critical Values are :",zcritical_l,zcritical_u)

if zstatistics<zcritical_l or zstatistics>zcritical_u:
    print("Reject the Null Hypothesis")
    print("sample is not from the large population")
else:
    print("Fail to reject the Null Hypothesis")
    print("sample is from the large population")

    print("\n")

"""
2.A sample of 900 items has mean 3.4 and standard deviation 2.61. can the sample be regarded as drawn from a population with mean 3.25 at 1 percent level of significance
"""
print("Assignment 2")
n=900
sMean=3.4
pMean=3.25
pStd=2.61

zstatistics=(sMean - pMean)/(pStd/np.sqrt(n))
print("Zstatistics :",zstatistics)

from scipy.stats import norm

zcritical_l=norm.ppf(q=0.01/2)
zcritical_u=-zcritical_l
print("Critical Values are :",zcritical_l,zcritical_u)

if zstatistics<zcritical_l or zstatistics>zcritical_u:
    print("Reject the Null Hypothesis")
    print("sample is not from the large population")
else:
    print("Fail to reject the Null Hypothesis")
    print("sample is from the large population")


#%%
import numpy as np

#%%
# Example 1
x = np.arange(1.0, 8 + 1, 1.0)
y = np.array([6.9, 10.8, 9.3, 7.8, -0.7, -9.2, -22.1, -37.7])

x_mean = np.mean(x)
y_mean = np.mean(y)

b1 = np.dot(y_mean - y, x_mean - x) / np.sum(np.power(x_mean - x, 2))
b0 = y_mean - b1 * x_mean
sigma_squared = (
    1 / x.shape[0] * np.sum([(yi - b0 - b1 * xi) ** 2 for (xi, yi) in zip(x, y)])
)
print(f"b0 = {b0}")
print(f"b1 = {b1}")
print(f"sigma_squared = {sigma_squared}")

# %%
# Example 2
x = np.array([1.0, 3.0, 6.0, 10.0])
y = np.array([1.1, 2.9, 6.1, 10.1])

x_mean = np.mean(x)
y_mean = np.mean(y)

b1 = np.dot(y_mean - y, x_mean - x) / np.sum(np.power(x_mean - x, 2))
b0 = y_mean - b1 * x_mean
sigma_squared = (
    1 / x.shape[0] * np.sum([(yi - b0 - b1 * xi) ** 2 for (xi, yi) in zip(x, y)])
)
print(f"b0 = {b0}")
print(f"b1 = {b1}")
print(f"sigma_squared = {sigma_squared}")

# %%
# Example 3
T_denominator = np.sqrt(np.sum([(yi - b0 - b1 * xi) ** 2 for (xi, yi) in zip(x, y)]))
print(T_denominator)

sigma_prime = np.sqrt(
    (
        1
        / (x.shape[0] - 2)
        * np.sum([(yi - b0 - b1 * xi) ** 2 for (xi, yi) in zip(x, y)])
    )
)
sx = np.sqrt(np.sum([(xi - x_mean) ** 2 for xi in x]))
b0_distribution_denominator = sigma_prime * np.sqrt(
    1 / x.shape[0] + x_mean ** 2 / sx ** 2
)
print(b0_distribution_denominator)
print(T_denominator / b0_distribution_denominator)

T = (b0 - 0.448) / T_denominator
print(T)
print(T * T_denominator / b0_distribution_denominator)


import numpy as np

def identity(z):
    """Identity function...
    Args:
        z (np.array)

    Returns:
        f(z) = z (np.array)
    """
    return z

def dfdz_identity(z):
    """Derivative of the Identity function...
    Args:
        z (np.array)

    Returns:
        df(z)/dz = 1.0 (np.array)
    """
    return np.ones_like(z)

def sigmoid(z):
    """Sigmoid function...
    Args:
        z (np.array)

    Returns:
        f(z) = 1 / (1 + exp(-z)) (np.array)
    """
    return 1.0 / (1.0 + np.exp(-z))

def dfdz_sigmoid(z):
    """Derivative of the Sigmoid function...
    Args:
        z (np.array)

    Returns:
        df(z)/dz = f(z) * (1 - f(z)) (np.array)
    """
    return sigmoid(z) * (1.0 - sigmoid(z))

def logistic(z):
    """Logistic function...
    Args:
        z (np.array)

    Returns:
        f(z) = 1 / (1 + exp(-z)) (np.array)
    """
    return sigmoid(z)

def dfdz_logistic(z):
    """Derivative of the Logistic function...
    Args:
        z (np.array)

    Returns:
        df(z)/dz = f(z) * (1 - f(z)) (np.array)
    """
    return sigmoid(z) * (1.0 - sigmoid(z))

def tanh(z):
    """Hyperbolic tangent function...
    Args:
        z (np.array)

    Returns:
        f(z) = 2.0 / (1.0 + np.exp(-2.0 * z)) - 1.0 (np.array)
    """
    return 2.0 / (1.0 + np.exp(-2.0 * z)) - 1.0

def dfdz_tanh(z):
    """Derivative of the hyperbolic tangent function...
    Args:
        z (np.array)

    Returns:
        df(z)/dz = 1.0 - np.square(tanh(z)) (np.array)
    """
    return 1.0 - np.square(tanh(z))

def softsign(z):
    """Softsign function...
    Args:
        z (np.array)

    Returns:
        f(z) = z / (1.0 + np.abs(z)) (np.array)
    """
    return z / (1.0 + np.abs(z))

def dfdz_softsign(z):
    """Derivative of the softsign function...
    Args:
        z (np.array)

    Returns:
        df(z)/dz = None (np.array)
    """
    raise RuntimeError('not implemented...')

def ReLU(z):
    """Rectified linear unit function...
    Args:
        z (np.array)

    Returns:
        f(z) = np.max(0, z) (np.array)
    """
    return z * (z > 0)

def dfdz_ReLU(z):
    """Derivative of the rectified linear unit function...
    Args:
        z (np.array)

    Returns:
        df(z)/dz = 1 if x > 0 else 0 (np.array)
    """
    return (z > 0)

def LReLU(z):
    """Leaky rectified linear unit function...
    Args:
        z (np.array)

    Returns:
        f(z) = z if z > 0 else 0.01 * z (np.array)
    """
    return PReLU(z, 0.01)

def dfdz_LReLU(z):
    """Derivative of the leaky rectified linear unit function...
    Args:
        z (np.array)

    Returns:
        df(z)/dz = 1 if x > 0 else 0.01 (np.array)
    """
    return dfdz_PReLU(z, 0.01)

def PReLU(z, alpha):
    """Parametric rectified linear unit function...
    Args:
        z (np.array)

    Returns:
        f(z) = z if z > 0 else alpha * z (np.array)
    """
    return z * (z > 0) + alpha * z * (z <= 0)

def dfdz_PReLU(z, alpha):
    """Derivative of the parametric rectified linear unit function...
    Args:
        z (np.array)

    Returns:
        df(z)/dz = 1 if x > 0 else alpha (np.array)
    """
    return 1.0 * (z > 0) + alpha * (z <= 0)


import numpy as np
import matplotlib.pyplot as plt

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

def stable_coeff(rho, psi):
    r = sigmoid(rho)
    theta = np.pi * sigmoid(psi)

    a_1 = -2*r*np.cos(theta)
    a_2 = r**2
    return a_1, a_2

def roots_polynomial(a_1, a_2):
    delta = a_1**2 - 4 * a_2
    delta = delta.astype(np.complex)
    root_1 = (-a_1 + np.sqrt(delta))/2
    root_2 = (-a_1 - np.sqrt(delta))/2
    idx_real = delta > 0
    return root_1, root_2, idx_real

if __name__ == '__main__':

    N = 100000
    rho = np.random.randn(N)*1
    psi = np.random.randn(N)*1

    a_1, a_2 = stable_coeff(rho, psi)
    r_1, r_2, idx_real = roots_polynomial(a_1, a_2)

    fig, ax = plt.subplots()
    ax.plot(a_1, a_2, '*')
    ax.plot(a_1[idx_real], a_2[idx_real], 'k*')
    ax.set_xlabel('a_1')
    ax.set_ylabel('a_2')
    ax.set_xlim([-2, 2])
    ax.set_ylim([-2, 2])

    fig, ax = plt.subplots()
    ax.plot(np.real(r_1), np.imag(r_1), 'r*')
    ax.plot(np.real(r_2), np.imag(r_2), 'r*')
    ax.plot(np.real(r_1)[idx_real], np.imag(r_1)[idx_real], 'k*')
    ax.plot(np.real(r_2)[idx_real], np.imag(r_2)[idx_real], 'k*')
    ax.set_xlim([-1.2, 1.2])
    ax.set_ylim([-1.2, 1.2])

    perc_real = np.sum(idx_real) / N *100
    print(f"Real poles in {perc_real:.1f} cases")