import numpy as np

"""
Explanation: Python Numpy
<img src="images/numpylogo.svg" alt="matplotlib" style="width: 400px;"/>
Numpy is a numerical package used extensively in python coding. You can call the install the numpy package by 
pip install numpy
When you import a module, you can choose to bound an alias to the package. In python communities, we usually import the numpy module like this:
<a href="https://colab.research.google.com/github/ryan-leung/PHYS4650_Python_Tutorial/blob/master/notebooks/03-Python-Numpy-Array.ipynb"><img align="right" src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open in Colab" title="Open and Execute in Google Colaboratory">
</a>
End of explanation
"""


# 1D array
a = np.array([2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97])

print(a.shape) # Return the ``shape`` of the array
print(a)

# 2D array
b = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

print(b.shape) # Return the ``shape`` of the array
print(b)

"""
Explanation: A numpy array
A numpy array is a grid of values, all of the same type. The number of dimensions give the rank of the array.
To initilze a 1D array, we will do:
End of explanation
"""


print(a[0], a[3], a[5])

print(b[0,0],b[1,1],b[2,2])

"""
Explanation: To call or change the element in the array, we can apply similar operation as a list
End of explanation
"""


x = range(100000)
sum(x)

y = np.array(x)
np.sum(y)

%timeit sum(x)

%timeit np.sum(y)

"""
Explanation: Universal functions (ufunc)
A universal function (or ufunc for short) is a function that operates on numpy ndarrays in an element-by-element fashion that has been written in compiled C code. That is, a ufunc is a "vectorized" wrapper in high performance code.
End of explanation
"""


# Create an array of all zeros
np.zeros((5, 5))

# Create an array of all ones
np.ones((5,5))

# Create a constant array
np.ones((5,5)) * 7

np.full((5,5), 7)  

# Create a 3x3 identity matrix
np.eye(3)

"""
Explanation: Initlize arrays
End of explanation
"""


# Create an array filled with random values from 0 to 1
np.random.random((3,2))

# Create an 1D array filled with random integer from 1 to n
np.random.randint(1, 1000, 10)

# Seeding the random values will always give you the same "random" numbers on next run
# We put the answer to the Ultimate Question of Life, the Universe, and Everything to the seed integer
np.random.seed(42)

# Create an 1D array filled with random integer from 1 to n
np.random.randint(1, 1000, 10)

"""
Explanation: Pseudo-random number generators
End of explanation
"""


### Slicing

e = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
e

# Each dimensions slice similar to a list, here it means 1st dimension select all, 
# 2nd dimension select from one till end.
e[:,1:]

# Here it means 1st dimension and 2nd dimension select from start till 2, 
# i.e. the upper left part of the array.
e[:2,:2]

# An operation like numpy array > than a value return a boolean array
e > 5

# If we put the boolean array into the same array, it will select all element that satisfy the conditions
e[e > 5]

"""
Explanation: Array indexing
End of explanation
"""


