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

"""
Explanation: Numpy 2D arrays - some examples
This notebook demonstrates how to work with 2D numpy arrays, including array slicing, random numbers, and making plots with them.  Note that this works with higher-dimensional arrays as well!
Some useful links:

Numpy quickstart
A useful numpy tutorial
Numpy array creation methods
Numpy array slicing/indexing tutorial
Numpy array slicing and indexing techniques (more extensive documentation)
The Numpy random module
End of explanation
"""


a = np.zeros([8,10])
print(a)

"""
Explanation: Array creation and basic properties
Create an 8x10 array of zeros called my_array.  Note that you can do this with any numpy array method (ones, zeros_like, ones_like, etc.).  See this page for a full list of routines for array creation.
End of explanation
"""


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

"""
Explanation: You have already created a 1D numpy array of predetermined values by giving np.array a list.  You can make a multi-dimensional numpy array by giving  np.array a set of nested lists (i.e., a list of lists).  The following will create a 3x3 array with predetermined values:
End of explanation
"""


print("the shape of this array is:", a.shape)
print("there are:", a.ndim, "dimensions")
print("there are", a.size, "total elements")

for i in range(a.ndim):
    print("the size of dimension", i, "is", a.shape[i])

"""
Explanation: The array .shape property tells you how large the array is in each dimension, .ndim tells you the number of dimensions, and .size tells you the total number of elements in the array.  You can access each of the dimensions dim by .shape[dim].
End of explanation
"""


a[2,6]=11

#print entire array
print(a)

#print a single element of the array
print(a[2,6])

"""
Explanation: You can manipulate individual cells of an array by:
a[index_1,index_2]
Note that when you print it, the first index corresponds to rows (counting down from the top) and the second index corresponds to columns (counting from the left).  Indices in both directions count by zeros.
End of explanation
"""


# create a 1D array with values 0...10
c = np.arange(0,10)

# note: the '\n' at the beginning of many of the print statements 
# adds a carriage return (blank line)

print("some elements from the middle of the array:",c[3:7] )
print("\nthe second element through the second-to-last element:", c[2:-1]) 
print("\nthe first half of the array:", c[:5])
print("\nthe second half of the array:", c[5:])
print("\nevery other element from 2-8 (inclusive):",c[2:9:2])
print("\nevery third element in the array:",c[::3])
print("\nreverse the array:",c[-1::-1]) 

"""
Explanation: Slicing arrays
You can also use the same type of slicing that you use with lists - in other words, python allows you to select some subset of the elements in a list or an array to manipulate or copy.  With slicing, there are three values that can be used along each dimension: start,end, and step, separated by colons.  Here are some examples in 1D:
myarray[start,end]   # items start through end-1
myarray[start:]      # items start through the end of the array
myarray[:end]        # items from the beginning of the array through end-1
myarray[:]           # a copy of the whole array
myarray[start,end,step]  # every "step" item from start to end-1
myarray[::step]      # every "step" item over the whole array, starting with the first element.
Note that negative indices count from the end of the array, so myarray[-1] is the last element in the array, myarray[-2] is the second-to-last element, etc.  You can also reverse the order of the array by starting at the end and counting to the beginning by negative numbers -- in other words, myarray[-1::-1] starts at the end of the array and goes to the first element by counting down by one each time.
End of explanation
"""


print("the array b:\n",b,"\n")
#  To get a square array containing the first two elements along each dimension:
print("the first row:", b[0,:])

print("\nthe third column:",b[:,2])

print("\nevery other element of the second row, starting with element 0:",b[1,::2])

print("\nsquare array of first two elements along each dimension:\n",b[:2,:2])

print("\nsquare array of last two elements along each dimension:\n",b[-2:,-2:])

print("\nsquare array of every other element along each dimension:\n",b[::2,::2])

print("\nreversed array:\n",b[-1::-1,-1::-1])

"""
Explanation: The same sort of technique can be used with a multi-dimensional array, with start, stop, and (optionally) step specified along each dimension, with the dimensions separated by a comma.  The syntax would be:
my2Darray[start1:stop1:step1, start2:stop2:step2]
With the same rules as above.  You can also combine slicing with fixed indices to get some or all elements from a single row or column of your array.
For example,  array b created above is a 3x3 array with the values 1-9 stored in it.  We can do several different things:
b[0,:]     # get the first row
b[:,2]     # get the third column
b[1,::2]   # get every other element of the first row, starting at element 0
b[:2,:2]   # get a square array containing the first two elements along each dimension
b[-2:,-2:] # get a square array containing the last two elements along each dimension
b[::2,::2] # get a square array of every other element along each dimension
b[-1::-1,-1::-1]  # original sized array, but reversed along both dimensions
End of explanation
"""


c = np.full((4,4),10.0)  # makes an array of shape (4,4) where all elements are value 10.0

d = c

print("c:\n",c, "\nd:\n", d)

"""
Explanation: Copying arrays
So far, we've only shown you how to create arrays and manipulate subsets of arrays.  But what about copying arrays?  What happens when you create an array c, and set d=c?
End of explanation
"""


d[:,0] = -1.0  # make column 0 equal to -1
d[:,2] = -6.0  # make column 2 equal to -6

print("c:\n",c, "\nd:\n", d)

"""
Explanation: The two arrays are the same, which is what you would expect.  But, what happens if we make changes to array d?
End of explanation
"""


e = np.full((4,4),10.0)  # makes an array of shape (4,4) where all elements are value 10.0

f = np.copy(e)

f[:,0] = -1.0  # make column 0 equal to -1
f[:,2] = -6.0  # make column 2 equal to -6

print("e:\n",e, "\nf:\n", f)

"""
Explanation: Arrays c and d are identical, even though you only changed d!
So what's going on here?  When you equate arrays in Numpy (i.e., d = c), you create a reference, rather than copying the array -- in other words, the array d is not a distinct array, but rather points to the array c in memory.  Any modification to either c or d will be seen by both.  To actually make a copy, you have to use the np.copy() method:
End of explanation
"""


g = np.full((4,4),10.0)  # makes an array of shape (4,4) where all elements are value 10.0

h = np.copy(g[0:2,0:2])

print("g:\n",g, "\nh:\n", h)

"""
Explanation: You can also make a copy of a subset of an arrays:
End of explanation
"""


i = np.full((4,4),10.0)  # makes an array of shape (4,4) where all elements are value 10.0

j = i[0:2,0:2]

print("\nunmodified arrays:\n")

print("i:\n",i, "\nj:\n", j)

print("\narrays after modification:\n")

j[1,1]=-999.0

print("i:\n",i, "\nj:\n", j)


"""
Explanation: Note that you can also create an array that references a subset of another array rather than copies it, and manipulate that in any way you want.  The changes will then appear in both the new array and your original array.  For example:
End of explanation
"""


random_float_array = np.random.random((5,5))

print(random_float_array)

random_int_array = np.random.randint(0,10,(5,5))

print("\n",random_int_array)

"""
Explanation: Numpy and random numbers
Numpy has a random module that can be used to generate random numbers in a similar way to the standard Python random module, but with the added advantage that it can do so for arrays of values.  Two commonly-used methods are:

random, which generates an array with user-specified dimensions (1D, 2D, or more dimensions!) and fills it with random floating-point values in the interval [0,1).
randint, which generates an array with user-specified dimensions and fills it with random integers in a user-specified interval.
End of explanation
"""


new_rand_array = np.random.random((100,100))

plt.matshow(new_rand_array)

# uncomment the following line to save the figure to your hard drive!
#plt.savefig("myimage.png")

"""
Explanation: Plotting numpy arrays
It's easy to plot 2D Numpy arrays in matplotlib using the pyplot matshow method:
End of explanation
"""


myplot = plt.matshow(new_rand_array)
myplot.axes.get_xaxis().set_visible(False)
myplot.axes.get_yaxis().set_visible(False)

"""
Explanation: And you can turn off the array axes with the following incantation:
End of explanation
"""


# interpolation='none' keeps imshow() from trying to interpolate between values and
#   making it look fuzzy.
# cmap='mapname' changes the color map.
# vmin, vmax sets the range of the color bar (from 0.0 - 0.5 in this example)

myplot = plt.imshow(new_rand_array, interpolation='none',cmap='hot',vmin=0.0,vmax=0.5)

# uncomment the following lines to remove the axis labels
#myplot.axes.get_xaxis().set_visible(False)
#myplot.axes.get_yaxis().set_visible(False)

# uncomment the following line to save the figure to your hard drive!
#plt.savefig("myimage.png")

"""
Explanation: (See this page for a more complex example.)  Finally, you can use the pyplot imshow method to control many aspects of a plotted array, including things like the color map, opacity, and the minimum and maximum range.
End of explanation
"""


