'''
Part 1 - basic

Please solve the following exercises. Good Luck!
'''

'''
1. Import the numpy package under the name `np`
'''

import numpy as np

'''
2. Print the numpy version
hint: search on Google "numpy version", the first entries.
'''

print(np.version.version)

'''
3. Create a vector of zeros with the size 10
hint: use np.zeros()
'''

newVector = np.zeros(10)

'''
4. How to find the memory size of any array
hint: itemsize returns the size in memory of each element
'''

print(np.size(newVector))

'''
5. How to get the documentation of the numpy add function from the IPython console?
hint: try "?" before the function name
'''

?np.size

'''
6. Create a vector of zeros with the size 10 but the fifth value which is 1
'''

newVector = np.zeros(10)
newVector[4]=1
print(newVector)

'''
7. Create a vector with values ranging from 10 to 49 (★☆☆)
hint: np.arange()
'''

newVector = np.arange(10, 50, 1) # arguments: start, stop, step
print(newVector)

'''
8. Reverse a vector (first element becomes last) (★☆☆)
hint: [::-1]
'''

reversedVector = newVector[::-1]
print(reversedVector)

'''
9. Create a 3x3 matrix with values ranging from 0 to 8 (★☆☆)
hint: reshape
'''

newVector = np.arange(0, 9, 1)
reshapeVector = np.reshape(newVector,(3,3))
print(reshapeVector)

'''
10. Find indices of non-zero elements from \[1,2,0,0,4,0\] (★☆☆)
hint: nonzero
'''

newVector = np.array([1,2,0,0,4,0])
print(np.nonzero(newVector))

'''
11. Create a 3x3 identity matrix (★☆☆)
hint: the best numpy documentation is in the scipy website :)
try https://docs.scipy.org/doc/numpy/reference/generated/numpy.eye.html
'''
newIdentityMatrix = np.eye(3)

'''
12. Create a 3x3x3 array with random values (★☆☆)
hint: Random sampling (numpy.random) — NumPy v1.21 Manual
'''

newRandom3DMatrix = np.random.rand(3,3,3)
print(newRandom3DMatrix)

'''
13. Create a 10x10 array with random values and find the minimum and maximum values
hint: a.min,a.max
'''
newRandom3DMatrix = np.random.rand(10,10)
print(newRandom3DMatrix.max(), newRandom3DMatrix.min())

'''
14. Create a random vector of size 30 and find the mean value
hint: a.mean
'''

newRandomVector = np.random.rand(30)
print(newRandomVector.mean())

'''
15. Create a 2d array with 1 on the border and 0 inside
hint: ones()
hint2: for an array a, which elements are chosen by a[1:-1] ?
'''

newOnesArray = np.ones([5,5], dtype=int)
newOnesArray[1:-1, 1:-1] = 0
print(newOnesArray)

'''
16. How to add a border (filled with 0's) around an existing array?
hint: https://docs.scipy.org/doc/numpy/reference/generated/numpy.pad.html
'''

newOnesArray = np.ones([5,5], dtype=int)
newOnesArray = np.pad(newOnesArray, pad_width=1)
print(newOnesArray)

'''
17. What is the result of the following expression? (★☆☆)
```python
0 * np.nan
np.nan == np.nan
np.inf > np.nan
np.nan - np.nan
0.3 == 3 * 0.1
```
hint 1: nan is not a number
hint 2: try printing the expressions to see what is printed
'''

print(np.nan == np.nan) # False
print(np.inf > np.nan) # False
print(np.nan - np.nan) # nan
print(0.3 == 3 * 0.1) # False

'''
18. Create a 5x5 matrix with values 1,2,3,4,7 on the diagonal
hint: np.diag
'''

newDiagonalMartix = np.diag([1,2,3,4,7])
print(newDiagonalMartix)

'''
19. Create a 8x8 matrix and fill it with a checkerboard pattern
hint1: slicing an array you can use - list[start:end:step]
hint2: [::2] - for even. [1::2] - for odd
'''

checkerboardMatrix = np.ones([8,8],dtype=int)
for columnNumber in range(0,8):
    if columnNumber%2==0:
        checkerboardMatrix[columnNumber][::2] = 0
    else:
        checkerboardMatrix[columnNumber][1::2] = 0
print(checkerboardMatrix)

'''
20. Consider a (6,7,8) shape array, what is the index (x,y,z) of the 100th element?
hint: https://docs.scipy.org/doc/numpy-1.10.0/reference/generated/numpy.unravel_index.html
'''

print(np.unravel_index(100,(6,7,8)))

'''
21. Create a checkerboard 8x8 matrix using the tile function (★☆☆)
hint: numpy.tile(A, reps)
Construct an array by repeating A the number of times given by reps.
'''

singleLineArray = np.array([1,2,3,4,5,6,7,8])
newCheckerboardMatrix = np.tile(singleLineArray,(8,1))
print(newCheckerboardMatrix)

'''
22. Normalize a 5x5 random matrix
hint: create a random 5*5 matrix
subtract the min value, then divide by (max-min)
'''

newRandomMatrix = np.random.rand(5,5)
updatedRandomMatrix = newRandomMatrix - newRandomMatrix.min()
normalizer = newRandomMatrix.max()-newRandomMatrix.min()
print(updatedRandomMatrix/normalizer)

'''
23. Create an array of 2x4 with dtype numpy.int16. print the dtype of the array
'''

newArray = np.array([1,2,3], dtype=int16)
print(newArray.dtype())

'''
24. Multiply a 5x3 matrix by a 3x2 matrix (real matrix product)
hint: numpy.dot()
'''

firstMatrix = np.random.rand(5,3)
secondMatrix = np.random.rand(3,2)
print(np.dot(firstMatrix,secondMatrix))
print(firstMatrix@secondMatrix)

'''
25. Given a 1D array, negate all elements which are between 3 and 8, in place.
(hint: >, <=)
'''

randomArray = np.random.uniform(1,10,50)
randomArray = np.int64(randomArray)
print([value for value in randomArray if 3>value and value<8])

'''
26. What is the output of the following script? (★☆☆)
(hint: np.sum)
# Author: Jake VanderPlas
print(sum(range(5),-1))
from numpy import *
print(sum(range(5),-1))
'''

print(sum(range(5),-1)) # 9
from numpy import *
print(sum(range(5),-1)) # 10

'''
27. Consider an integer vector Z, which of these expressions are legal? (★☆☆)
Z**Z
2 << Z >> 2
Z <- Z
1j*Z
Z/1/1
Z<Z>Z
'''

Z = np.random.uniform(1,100,100)
print(Z**Z)        # ok
print(2 << Z >> 2) # the inputs could not be safely coerced to any supported types
print(Z <- Z)      # ok, all FALSE
print(1j*Z)        # ok
print(Z/1/1)       # ok
print(Z<Z>Z)       # The truth value of an array with more than one element is ambiguous

'''
28. What are the result of the following expressions?
np.array(0) / np.array(0)
np.array(0) // np.array(0)
np.array([np.nan]).astype(int).astype(float)
'''

print(np.array(0) / np.array(0))  # nan
print(np.array(0) // np.array(0))  # 0
print(np.array([np.nan]).astype(int).astype(float)) # [-2.14748365e+09]

'''
29. How to round away from zero a float array ? (★☆☆)
(hint: np.uniform, np.copysign, np.ceil, np.abs)
'''

newArray = np.array([1.1,2.2,3.3,4.4,5.5])
absArray = np.ceil(newArray)
print(absArray)

'''
30. How to find common values between two arrays? (★☆☆)
(hint: np.intersect1d)
'''

a1 = np.int64(np.random.uniform(1,100,100))
a2 = np.int64(np.random.uniform(1,100,100))
print(np.intersect1d(a1, a2))

'''
31. How to ignore all numpy warnings (not recommended)? (★☆☆)
(hint: np.seterr, np.errstate)
'''

np.errstate(all='ignore')

'''
32. Is the following expressions true? (★☆☆)
(hint: imaginary number)
np.sqrt(-1) == np.emath.sqrt(-1)
'''

# nope

'''
33. How to get the dates of yesterday, today and tomorrow? (★☆☆)
(hint: np.datetime64, np.timedelta64)
'''

timeData = np.datetime64('today')
print(timeData)
print(timeData + np.timedelta64(1, 'D'))
print(timeData - np.timedelta64(1, 'D'))

'''
34. How to get all the dates corresponding to the month of July 2016? (★★☆)
(hint: np.arange(dtype=datetime64['D']))
'''

print(np.arange('2016-06', '2016-07', dtype='datetime64[D]'))

'''
36. Extract the integer part of a random array using 5 different methods (★★☆)
(hint: %, np.floor, np.ceil, astype, np.trunc)
'''

randomArray1 = np.random.uniform(1,10,5)
print(randomArray1)
randomArray1 = np.int64(randomArray1)
print(randomArray1)

randomArray2 = np.random.uniform(1,10,5)
print(randomArray2)
randomArray2 = np.floor(randomArray2)
print(randomArray2)

randomArray3 = np.random.uniform(1,10,5)
print(randomArray3)
randomArray3 = randomArray3.astype('int64')
print(randomArray3)

randomArray4 = np.random.uniform(1,10,5)
print(randomArray4)
randomArray4 = np.trunc(randomArray4)
print(randomArray4)

randomArray5 = np.random.uniform(1,10,5)
print(randomArray5)
randomArray5 = randomArray5 - randomArray5 % 1
print(randomArray5)
65

'''
37. Create a 5x5 matrix with row values ranging from 0 to 4 (★★☆)
(hint: np.arange)
'''

gradedArray = np.zeros((5,5))
gradedArray += np.arange(5)
print(gradedArray)

'''
39. Create a vector of size 10 with values ranging from 0 to 1, both excluded (★★☆)
(hint: np.linspace)
'''

gradedVector = np.linspace(0.1,1,num=10,endpoint=False)
print(gradedVector)

'''
40. Create a random vector of size 10 and sort it (★★☆)
(hint: sort)
'''

sortedArray = np.random.uniform(1,10,10)
sortedArray = np.sort(sortedArray)
print(sortedArray)

'''
41. How to sum a small array faster than np.sum? (★★☆)
(hint: np.add.reduce)
'''

newArray = np.random.uniform(1,10,10)
print(np.add.reduce(newArray))

'''
42. Consider two random array A and B, check if they are equal (★★☆)
(hint: np.allclose, np.array_equal)
'''

newArray1 = np.random.uniform(1,10,10)
newArray2 = np.random.uniform(1,10,10)
print(np.array_equal(newArray1,newArray2))
newArray1 = np.zeros((5))
newArray2 = np.zeros((5))
print(np.array_equal(newArray1,newArray2))

'''
43. Make an array immutable (read-only) (★★☆)
(hint: flags.writeable)
'''

newArray = np.random.uniform(1,10,10)
print(newArray.flags.writeable)
newArray.flags.writeable = False
print(newArray.flags.writeable)
newArray[0] = 1  # ValueError: assignment destination is read-only

'''
44. Consider a random 10x2 matrix representing cartesian coordinates, convert them to
polar coordinates (★★☆)
(hint: np.sqrt, np.arctan2)
'''

cartesianArray = np.reshape(np.int64(np.random.uniform(1,10,20)),(10,2))
print(cartesianArray)
polarArray = []
for column in cartesianArray:
    polarArray.append(np.sqrt(np.power(column[0],2)+np.power(column[1],2)))
    polarArray.append(np.arctan2(column[1],column[0]))
polarArray = np.reshape(polarArray,(10,2))
print(polarArray)

'''
45. Create random vector of size 10 and replace the maximum value by 0 (★★☆)
(hint: argmax)
'''

newArray = np.random.uniform(1,10,10)
print(newArray)
newArray[np.argmax(newArray)] = 0
print(newArray)

'''
48. Print the minimum and maximum representable value for each numpy scalar type
(★★☆)
(hint: np.iinfo, np.finfo, eps)
'''

for dtype in [np.int8, np.int32, np.int64]:
    print(dtype)
    print('min: ', np.iinfo(dtype).min)
    print('max: ', np.iinfo(dtype).max, '\n')
for dtype in [np.float32, np.float64]:
    print(dtype)
    print('min: ', np.finfo(dtype).min)
    print('max: ', np.finfo(dtype).max)

'''
50. How to find the closest value (to a given scalar) in a vector? (★★☆)
(hint: argmin)
'''

newArray = np.int64(np.random.uniform(1,50,20))
startingValue = np.int64(np.random.uniform(1,50,1))
nearestValueIndex = (np.abs(newArray-startingValue)).argmin()
print(newArray, startingValue, newArray[nearestValueIndex])

'''
52. Consider a random vector with shape (100,2) representing coordinates, find point by
point distances (★★☆)
(hint: np.atleast_2d, T, np.sqrt, think what is the equation for calculating distances)
'''

randomVector = np.reshape(np.int64(np.random.uniform(1,10,200)),(100,2))
xValues = np.atleast_2d(randomVector[:,0])
xValuesT = np.transpose(xValues)
yValues = np.atleast_2d(randomVector[:,1])
yValuesT = np.transpose(yValues)

distancesArray = []
distancesArray = np.sqrt((xValues-xValuesT)**2 + (yValues-yValuesT)**2)
print(distancesArray)

'''
53. How to convert a float (32 bits) array into an integer (32 bits) in place?
(hint: astype(copy=False))
'''

randomArray = np.random.uniform(1,10,200)
randomArray.astype('float32')
randomArray.astype('int32')

'''
58. Subtract the mean of each row of a matrix (★★☆)
(hint: mean(axis=,keepdims=))
'''

randomMatrix = np.reshape(np.int64(np.random.uniform(1,10,100)),(10,10))

print('sum: ' , np.sum(randomMatrix,axis=1))
print('mean: ' , np.mean(randomMatrix,axis=1))

'''
60. How to tell if a given 2D array has null columns? (★★☆)
(hint: any, ~)
'''

randomMatrix = np.reshape(np.random.uniform(1,10,100),(10,10))
print((~randomMatrix.any(axis=0)).any())
randomMatrix[5][5] = np.nan
randomMatrix[4][4] = 0
print((~randomMatrix.any(axis=0)).any())

'''
64. Consider a given vector, how to add 1 to each element indexed by a second vector (be
careful with repeated indices)? (★★★)
(hint: np.bincount | np.add.at)
'''

newVector = np.random.randint(1,50,20)
print(newVector)

onesVector = np.int64(np.ones(20))
newVector = newVector + onesVector
print(newVector)

'''
67. Considering a four dimensions array, how to get sum over the last two axes at once?
(★★★)
(hint: sum(axis=(-2,-1)))
'''

#new4DArray = np.reshape(np.random.randint(1,50,625),(5,5,5,5))
new4DArray = np.reshape(np.int64(np.ones(625)),(5,5,5,5))
print(new4DArray)

print(np.sum(new4DArray,axis=(-2,-1)))

'''
68. Considering a one-dimensional vector D, how to compute means of subsets of D using a
vector S of same size describing subset indices? (★★★)
(hint: np.bincount)
'''

newVector1 = np.random.randint(1,50,20)
print(newVector1)
newVector2 = np.random.randint(1,50,20)
print(newVector2)
print(np.bincount(newVector2, weights=newVector1) / np.bincount(newVector2))

'''
69. How to get the diagonal of a dot product? (★★★)
(hint: np.diag)
'''

newVector1 = np.random.randint(1,50,(5,5))
newVector2 = np.random.randint(1,50,(5,5))
newVector2Transposed = np.transpose(newVector2)
print(np.sum(newVector1 * newVector2Transposed, axis=1))

'''
70. Consider the vector [1, 2, 3, 4, 5], how to build a new vector with 3 consecutive zeros
interleaved between each value? (★★★)
(hint: array[::4])
'''

newVector = np.array([1,2,3,4,5])
updatedVector = []
for i in range(0,len(newVector)-1):
    updatedVector.append(newVector[i])
    for j in range(0,3):
        updatedVector.append(0)
updatedVector.append(newVector[len(newVector)-1])
print(updatedVector)

'''
71. Consider an array of dimension (5,5,3), how to multiply it by an array with dimensions
(5,5)? (★★★)
(hint: array[:, :, None], or np.expand_dims)
'''

new3DArray = np.random.randint(1,50,(3,5,5))
new2DArray = np.random.randint(1,50,(5,5))

multipyOutcome = np.ones((3,5,5))

for i in range(0,3):
    multipyOutcome[:][:][i] = new3DArray[:][:][i] * new2DArray

print(multipyOutcome)

'''
72. How to swap two rows of an array? (★★★)
(hint: array[[]] = array[[]])
'''

newArray = np.random.randint(1,50,(2,5))
print(newArray)
newArray[[1,0]] = newArray[[0,1]]
print(newArray)

'''
75. How to compute averages using a sliding window over an array? (★★★)
(hint: np.cumsum)
'''
newArray = np.random.randint(1,50,(2,5))
print(newArray)
print(np.cumsum(newArray))

'''
77. How to negate a Boolean, or to change the sign of a float in place? (★★★)
(hint: np.logical_not, np.negative)
'''

zeroesOnces = np.random.randint(0,2,10)
print(zeroesOnces)
print(np.logical_not(zeroesOnces, out=zeroesOnces))

flipSignsArray = np.random.uniform(-1.0,1.0,10)
print(flipSignsArray)
print(np.negative(flipSignsArray, out=flipSignsArray))

'''
78. Consider 2 sets of points P0,P1 describing lines (2d) and a point p, how to compute
distance from p to each line i (P0[i],P1[i])? (★★★)
'''
def distance_from_2_lines_to_point(P0,P1,singlePoint):

    distanceP0 = np.power(P0-singlePoint,2)
    distanceP0sum = np.sum(distanceP0, axis = 1)
    distanceP0sumFinal = np.sqrt(distanceP0sum)

    distanceP1 = np.power(P1-singlePoint,2)
    distanceP1sum = np.sum(distanceP1, axis = 1)
    distanceP1sumFinal = np.sqrt(distanceP1sum)

    print(distanceP0sumFinal)
    print(distanceP1sumFinal)

P0 = np.random.randint(1,50,(5,2))
P1 = np.random.randint(1,50,(5,2))
singlePoint = np.random.randint(1,50,(1,2))
print('the distances from point ' , singlePoint)
print(distance_from_2_lines_to_point(P0,P1,singlePoint))

'''
79. Consider 2 sets of points P0,P1 describing lines (2d) and a set of points P, how to
compute distance from each point j (P[j]) to each line i (P0[i],P1[i])? (★★★)
'''

P0 = np.random.randint(1,50,(5,2))
P1 = np.random.randint(1,50,(5,2))
singlePointArray = np.random.randint(1,50,(5,2))

for startingPoint in singlePointArray:
    print('the distances from point ' , startingPoint)
    distance_from_2_lines_to_point(P0,P1,startingPoint)

'''
80. Consider an arbitrary array, write a function that extract a subpart with a fixed shape and
centered on a given element (pad with a fill value when necessary) (★★★)
(hint: minimum, maximum)
'''

import random
arbitraryArray = np.random.randint(1,10,(random.randint(1,10),random.randint(1,10)))
fixedShape = (random.randint(1,10),random.randint(1,10))
startingPoint = (random.randint(1,len(arbitraryArray[0])),random.randint(1,len(arbitraryArray[0])))

newArray = np.zeros((fixedShape[0],fixedShape[1]))

i = startingPoint[0]
j = startingPoint[1]

newarrayi = 0
newarrayj = 0

while i < len(arbitraryArray[0]):

    while j < len(arbitraryArray[1]):

        if newarrayi < fixedShape[0] and newarrayj < fixedShape[1]:
           newArray[newarrayi][newarrayj] = arbitraryArray[i][j]

        newarrayj += 1
        j += 1

    newarrayj = 0
    j = startingPoint[1]

    newarrayi += 1
    i += 1

print(newArray)

'''
83. How to find the most frequent value in an array?
(hints: np.unique, return_counts, argmax)
'''

newArray = np.random.randint(1,50,10)
print(np.bincount(newArray).argmax())

'''
89. How to get the n largest values of an array (★★★)
(hint: np.sort)
'''

import random
nValue =  random.randint(1,1000)
newArray = np.random.randint(1,100,1000)
sortedArray = np.sort(newArray)
print(sortedArray[-nValue:])

'''
93. Consider two arrays A and B of shape (8,3) and (2,2). How to find rows of A that contain
elements of each row of B regardless of the order of the elements in B? (★★★)
(hint: np.where)
'''

A = np.random.randint(1,10,(8,3))
B = np.random.randint(1,10,(2,2))

for singleRowA in A:
    for singleRowB in B:
        if singleRowB[0] in singleRowA and singleRowB[1] in singleRowA:
            print(singleRowB, 'was found i ', singleRowA)

'''
94. Considering a 10x3 matrix, extract rows with unequal values (e.g. [2,2,3]) (★★★)
'''

newMatrix = np.random.randint(1,3,(10,3))
print(newMatrix)
for singleRow in newMatrix:
    if singleRow[0] != singleRow[1] or singleRow[0] != singleRow[2]:
        print(singleRow)

'''
96. Given a two dimensional array, how to extract unique rows? (★★★)
(hint: np.ascontiguousarray | np.unique)
'''

twoDimensionalArray = np.random.randint(1,3,(10,3))
print(twoDimensionalArray,'\n')
print(np.unique(twoDimensionalArray, axis=0))


from math import sqrt

import numpy as np
from scipy.linalg import cholesky

class UnscentedTransform:
    def __init__(self, f, alpha, beta, k):
        self.f = f
        self.alpha = alpha
        self.beta = beta
        self.k = k
        self.l = None
        self.wm0 = None
        self.wmi = None
        self.wc0 = None
        self.wci = None

    def __call__(self, x, p):
        x = np.asarray(x).reshape((-1, 1))
        p = np.atleast_2d(np.asarray(p))
        n = len(x)

        if self.l is None:
            self.l = self.alpha**2 * (n + self.k) - n
            self.wm0 = self.l / (n + self.l)
            self.wc0 = self.l / (n + self.l) + 1 - self.alpha**2 + self.beta
            self.wmi = 1 / (2 * (n + self.l))
            self.wci = 1 / (2 * (n + self.l))

        # sqrt((L + lambda) * P)
        sqrtllp = sqrt(n + self.l) * cholesky(p, lower=False)

        # Construct sigma points and propagate
        sigmas = [x]
        ys = [np.asarray(self.f(x)).reshape((-1, 1))]
        for i in range(1, n + 1):
            delta = sqrtllp[:, i - 1].reshape((-1, 1))

            sigma = x + delta
            y = np.asarray(self.f(sigma)).reshape((-1, 1))
            sigmas.append(sigma)
            ys.append(y)

            sigma = x - delta
            y = np.asarray(self.f(sigma)).reshape((-1, 1))
            sigmas.append(sigma)
            ys.append(y)

        # Estimate mean and covariance
        mu = self.wm0 * ys[0]
        for y in ys[1:]:
            mu += self.wmi * y

        su = self.wc0 * ((ys[0] - mu) @ (ys[0] - mu).T)
        for y in ys[1:]:
            su += self.wci * ((y - mu) @ (y - mu).T)

        cu = self.wc0 * ((sigmas[0] - x) @ (ys[0] - mu).T)
        for sigma, y in zip(sigmas, ys[1:]):
            cu += self.wci * ((sigma - x) @ (y - mu).T)

        return mu.squeeze(), su, cu

if __name__ == '__main__':
    A = np.array([[1., -2.1],
                  [1.4, 4.1]])

    def f(x):
        return A @ x

    x0 = [1.3, -2]
    p0 = np.array([[7, 1.2],
                   [1.2, 8.4]])

    ut = UnscentedTransform(f, 1e-1, 2, (3 - 2))

    x1, sk, ck = ut(x0, p0)

    print(x1, A @ x0)
    print()

    print(sk, "\n")
    print(A @ p0 @ A.T, "\n")
    print(sk - A @ p0 @ A.T, "\n")


import numpy as np

class Cost():

    def __call__(self, AL, Y):
        raise NotImplementedError

    def bprop(self, AL, Y):
        raise NotImplementedError

class BinaryCrossEntropy(Cost):

    def __call__(self, AL, Y):
        m = Y.shape[1]
        cost = -(np.dot(Y, np.log(AL).T) + np.dot(1 - Y, np.log(1 - AL).T)) / m

        cost = np.squeeze(cost)  # To make sure your cost's shape is what we expect (e.g. this turns [[17]] into 17).
        assert (cost.shape == ())
        return cost

    def bprop(self, AL, Y):
        dAL = - (np.divide(Y, AL) - np.divide(1-Y, 1-AL))
        assert (dAL.shape == AL.shape)
        return dAL

class CrossEntropy(Cost):

    def __call__(self, AL, Y):
        m = Y.shape[1]
        # log_likelihood = -np.multiply(Y, np.log(AL))
        #
        # loss = np.sum(log_likelihood) / m

        sum = -np.sum(Y * np.log(AL)) / m

        return sum

    def bprop(self, AL, Y):

        return AL - Y

class SquaredError(Cost):

    def __call__(self, AL, Y):
        res = (AL - Y) ** 2 / 2
        return np.sum(res)

    def bprop(self, AL, Y):
        return AL - Y


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

import numpy as np
import numba
import random

class ImageModelDriver:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return [x_array, y_array]

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

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

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

        all_freqs = max_auto_freq + max_mutal_freq
        freqs_counts = {}

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

        result_freqs = []

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

        return result_freqs

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

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

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

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

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

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

        return True

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

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

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

        image.update(final_image, '_salt_and_pepper')

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

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

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

        return np.array(result_row)

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

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

        image.update(final_image, '_random_noize')

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

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

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

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

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

        output = np.zeros(image.shape)

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

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

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

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

        processed_image = np.array(output)

        image_sd.update(processed_image, '_linear_filter')

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

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

        output = np.zeros(image.shape)

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

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

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

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

        processed_image = np.array(output)

        image_sd.update(processed_image, '_median_filter')

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

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

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

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

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

        ImageModelDriver.grayscale(image_sd)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        processed_image = ImageModelDriver.convolution_2d(image_sd, mask)

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

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

        processed_image = ImageModelDriver.convolution_2d(image_sd, mask)

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

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

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

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

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

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

        processed_image = ImageModelDriver.convolution_2d(image_sd, mask)

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

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

        processed_image = ImageModelDriver.convolution_2d(image_sd, mask)

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

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

        processed_image = ImageModelDriver.convolution_2d(image_sd, mask)

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

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

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

        output = np.zeros_like(image)

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

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

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

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

        return output

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

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

        orig_shape = image_src.shape
        pad_width = dilation_level - 2

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

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

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

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

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

        orig_shape = image_src.shape
        pad_width = erosion_level - 2

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

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

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

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

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

        orig_shape = image_src.shape
        pad_width = erosion_level - 2

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

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

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

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

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

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

            if value > final_value:
                final_thresh = t
                final_value = value

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

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

        return  final_img

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

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

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

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

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


import numpy as np
from sklearn.neighbors import KNeighborsClassifier
from sklearn.cross_validation import KFold, cross_val_score
from itertools import combinations
from matplotlib.colors import ListedColormap
import matplotlib.pyplot as plt
# from sklearn.pipeline import Pipeline
# from sklearn.preprocessing import StandardScaler
from sklearn import preprocessing

data = np.genfromtxt('seeds_dataset.txt')
features, labels = data[:, 0:7], data[:, -1]

classifier = KNeighborsClassifier(n_neighbors=1)

errs = []
for train, test in KFold(features.shape[0], 5, True):
    classifier.fit(features[train], labels[train])
    pred = classifier.predict(features[test])
    err = (pred == labels[test]).mean()
    errs.append(err)
print '%.3f' % np.mean(errs)

for i1, i2 in combinations(range(features.shape[1]), 2):
    features2 = features[:, (i1, i2)]
    errs = []
    for train, test in KFold(features.shape[0], 5, True):
        classifier.fit(features2[train], labels[train])
        pred = classifier.predict(features2[test])
        errs.append((pred == labels[test]).mean())
    print i1, i2, '%.3f' % np.mean(errs)

featureNames = ['area', 'perimeter', 'compactness', 'length of kernel', 'width of kernel', 'asymmetry coefficient', 'length of kernel groove']

def plot_2d_knn_decision(featureIndices, features, labels, num_neighbors=1, norm=False):
    '''
    this function is taken from:
    https://github.com/grehujt/BuildingMachineLearningSystemsWithPython/blob/master/ch02/figure4_5_sklearn.py
    '''
    if norm:
        f1 = preprocessing.scale(features[:, featureIndices[0]])
        f2 = preprocessing.scale(features[:, featureIndices[1]])
        x0, x1 = f1.min() * 1.1, f1.max() * 1.1
        y0, y1 = f2.min() * 1.1, f2.max() * 1.1
    else:
        f1 = features[:, featureIndices[0]]
        f2 = features[:, featureIndices[1]]
        x0, x1 = f1.min() * .9, f1.max() * 1.1
        y0, y1 = f2.min() * .9, f2.max() * 1.1

    X = np.linspace(x0, x1, 1000)  # shape: (1000,)
    Y = np.linspace(y0, y1, 1000)  # shape: (1000,)
    X, Y = np.meshgrid(X, Y)  # X.shape: (1000, 1000), Y.shape: (1000, 1000)

    model = KNeighborsClassifier(num_neighbors)
    # model = Pipeline([
    #     ('norm', StandardScaler()),
    #     ('knn', model)
    # ])
    fs = np.vstack((f1, f2)).T
    model.fit(fs, labels)
    C = model.predict(np.vstack([X.ravel(), Y.ravel()]).T).reshape(X.shape)
    cmap = ListedColormap([(1., .7, .7), (.7, 1., .7), (.7, .7, 1.)])

    fig, ax = plt.subplots()  # concise way of "fig = plt.figure(); ax = fig.add_subplot(111)"
    ax.set_xlim(x0, x1)
    ax.set_ylim(y0, y1)
    ax.set_xlabel(featureNames[featureIndices[0]])
    ax.set_ylabel(featureNames[featureIndices[1]])
    ax.pcolormesh(X, Y, C, cmap=cmap)

    cmap = ListedColormap([(1., .0, .0), (.1, .6, .1), (.0, .0, 1.)])
    # c=labels, use labels to color data points, mapping to cmap
    ax.scatter(f1, f2, c=labels, cmap=cmap)
    print '3 fold cross validation mean error:', cross_val_score(model, fs, labels).mean()
    return fig, ax

fig, ax = plot_2d_knn_decision([3, 6], features, labels, 1, False)
# 3 fold cross validation mean error: 0.924114331723
fig.suptitle('k = 1 on feature 3 & 6')
fig.savefig('./pics/figure1.png')

fig, ax = plot_2d_knn_decision([0, 2], features, labels, 1, False)
# 3 fold cross validation mean error: 0.794082125604
fig.suptitle('k = 1 on feature 0 & 2')
fig.savefig('./pics/figure2.png')

fig, ax = plot_2d_knn_decision([3, 6], features, labels, 1, True)
# 3 fold cross validation mean error: 0.933574879227
fig.suptitle('k = 1 on feature 3 & 6 with norm')
fig.savefig('./pics/figure1.1.png')

fig, ax = plot_2d_knn_decision([0, 2], features, labels, 1, True)
# 3 fold cross validation mean error: 0.785628019324
fig.suptitle('k = 1 on feature 0 & 2 wth norm')
fig.savefig('./pics/figure2.1.png')

fig, ax = plot_2d_knn_decision([3, 6], features, labels, 5, True)
# 3 fold cross validation mean error: 0.928341384863
fig.suptitle('k = 5 on feature 3 & 6 with norm')
fig.savefig('./pics/figure3.1.png')

fig, ax = plot_2d_knn_decision([3, 6], features, labels, 10, True)
# 3 fold cross validation mean error: 0.938204508857
fig.suptitle('k = 10 on feature 3 & 6 with norm')
fig.savefig('./pics/figure3.2.png')

fig, ax = plot_2d_knn_decision([3, 6], features, labels, 20, True)
# 3 fold cross validation mean error: 0.895933977456
fig.suptitle('k = 20 on feature 3 & 6 with norm')
fig.savefig('./pics/figure4.1.png')


"""
hill_cipher.py

@author Elliot and Erica
"""

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

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

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

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

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

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

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

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

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

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

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

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

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


import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
from sklearn.linear_model import LogisticRegression

# Logistic Regression on Data

def get_heart_datset():
    return pd.read_csv("../datasets/SAheart/SAheart.data", index_col=0)

def norm(x):
    x = (x - x.mean()) / x.std()
    return x

def get_sigma(y, y_hat, p):
    y_delta = y - y_hat
    y_delta_sq = y_delta.T @ y_delta
    sigma = np.sqrt(y_delta_sq / (len(y) - p - 1))
    return sigma

def get_std_errors(x, y, y_hat):
    x_cpy = x.copy()
    x_cpy["intercept"] = 1
    df = x_cpy.transpose().dot(x_cpy)
    inv = np.linalg.inv(df.values) * get_sigma(y, y_hat, x.shape[1])
    return np.sqrt(np.diag(inv))

def logistic_regression(x_train, y_train):
    regr = LogisticRegression(solver='newton-cg', penalty='none')
    regr.fit(x_train, y_train)
    return regr

if __name__ == '__main__':
    sa_heart = get_heart_datset()
    features = sa_heart.columns
    sa_heart["famhist"] = sa_heart["famhist"].astype('category').cat.codes
    selected_features = [features[i] for i in [0, 1, 2, 4, 6, 7, 8]]
    print(selected_features)
    x = sa_heart[selected_features]
    y = sa_heart["chd"]

    regr = logistic_regression(x, y)
    y_hat = regr.predict(x)
    std_errors = get_std_errors(x, y, y_hat)

    print("Intercept: %2.3f" % regr.intercept_)
    for i in range(len(selected_features)):
        print(selected_features[i]+": %2.3f , %2.5f"%(regr.coef_[0][i], std_errors[i] ))


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

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

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

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

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

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

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


import numpy as np
import matplotlib.pyplot as plt

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

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

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

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

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

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

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

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

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

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

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


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

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

numSamples = 100000
numBins = 100

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

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

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

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

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

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


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

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

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

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

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

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

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

loss_type = 3

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

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

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

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

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

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

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

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

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


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

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

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

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

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


import numpy as np

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

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

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

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

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

    return A

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

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

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

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

    return A

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

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

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

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

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

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

    return dZ

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

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

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

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

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

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

    return dZ


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

class HeatEquationSolver:

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

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

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

    self.alpha = alpha

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

    self.save_plots = save_plots

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

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

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

    return 0

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

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

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

    return u

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

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

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

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

    u_solution = solver.get_solution()

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

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

    u_solution = solver.get_solution()

    solver.plot_stacked(u_solution)

if __name__ == '__main__':
    test_2()


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

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

# cm is a confusion matrix

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        cm = confusion_matrix(val[label], pred)

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

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

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

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

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

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

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

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

        m = model(train_features, train_labels)

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

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

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

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

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

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

    return train_acc_history, val_acc_history

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

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

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

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

    return max_acc, arg_max


import numpy as np
import pytest
from scipy.special import k0

from structure_factor.transforms import HankelTransformOgata
from structure_factor.utils import bessel1, bessel1_zeros, bessel2

def integrate_against_abs_monomial(f, nu=0, h=0.1, n=100, f_even=False):
    # Section 1 Ogata https://www.kurims.kyoto-u.ac.jp/~okamoto/paper/Publ_RIMS_DE/41-4-40.pdf
    x = bessel1_zeros(nu, n)
    weights = bessel2(nu, x) / bessel1(nu + 1, x)  # equation 1.2
    x *= h / np.pi  # equivalent of xi variable
    # equation 1.1
    deg = 2 * nu + 1
    if f_even:
        return 2.0 * h * np.sum(weights * np.power(x, deg) * f(x), axis=-1)
    return h * np.sum(weights * np.power(x, deg) * (f(x) + f(-x)), axis=-1)

# See Section 4 in Ogata https://www.kurims.kyoto-u.ac.jp/~okamoto/paper/Publ_RIMS_DE/41-4-40.pdf

def test_section4_example1():
    f = lambda x: np.exp(-np.cosh(x)) / (1 + np.square(x))
    order = 0
    actual = integrate_against_abs_monomial(f, order)
    desired = 0.306354694925705
    np.testing.assert_almost_equal(actual, desired)

def test_section4_example2():
    f = lambda x: np.exp(-np.square(x))
    order = 0
    actual = integrate_against_abs_monomial(f, order)
    desired = 1.0
    np.testing.assert_almost_equal(actual, desired)

# See Section 5 paragraph "Numerical Examples" in Ogata https://www.kurims.kyoto-u.ac.jp/~okamoto/paper/Publ_RIMS_DE/41-4-40.pdf

@pytest.fixture
def ht():
    order = 0
    ht = HankelTransformOgata(order)
    ht.compute_transformation_parameters(step_size=0.01, nb_points=300)
    return ht

def test_section5_example1(ht):
    f = lambda r: 1.0 / r
    _, actual = ht.transform(f, 1)
    desired = 1.0
    np.testing.assert_almost_equal(actual, desired)

def test_section5_example2(ht):
    f = lambda r: 1.0 / (1.0 + r ** 2)
    _, actual = ht.transform(f, 1)
    desired = k0(1)
    np.testing.assert_almost_equal(actual, desired)


import numpy as np

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

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

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

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

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

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

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

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

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

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

print()

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


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

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

# Here I first set conditions

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

print('\n')

# Basic definitions
initialLineValue = 0
initialColumnValue = 0

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

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

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

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

# Here I set the Matrix

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

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

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

resultMatrix = np.zeros(pointsAmount)

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

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

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

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

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

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

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

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

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

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

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

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


# Problemas (Física)

## Problema (Movimento de partícula)

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

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

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

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

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

(Adaptado de Halliday & Resnick)

### Resolução

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

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

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

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

# b. c. e d.

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

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

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

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

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


#!/usr/bin/env python

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

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

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

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

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

exact_diff = np.cos(x_0)

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

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

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

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

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

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


import random
from math import sin
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
import numpy as np

def detrend(ts):
    X = [[i] for i in range(len(ts))]
    y = np.array(ts).reshape(-1, 1)
    reg = LinearRegression().fit(X, y)
    a = reg.coef_[0][0]
    b = reg.intercept_[0]
    detrend_ts = [(ts[i] - a * i - b) for i in range(len(ts))]
    return detrend_ts, a, b

def retrend(ts, a, b):
    return [(ts[i] + a * i + b) for i in range(len(ts))]

if __name__ == '__main__':

    random.seed(1)

    ts = [10 + .8 * i + sin(i) + 3 * random.random() for i in range(20)]
    detrend_ts, a, b = detrend(ts)
    retrend_ts = retrend(detrend_ts, a, b)

    fig = plt.figure()

    ax1 = fig.add_subplot(311)
    ax2 = fig.add_subplot(312)
    ax3 = fig.add_subplot(313)

    ax1.set_title("Trended Time Series")
    ax1.plot(ts)

    ax2.set_title("Detrended Time Series")
    ax2.plot(detrend_ts)

    ax3.set_title("Retrended Time Series")
    ax3.plot(retrend_ts)

    plt.show()


import numpy as np

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

def diagonalyDominantCheck(A):
    '''

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

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

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

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

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

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

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

        x = nextx

    return x

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

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

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

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

    return x

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return D

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return Mp, Np, Ms, Ns