import numpy as np

from scipy.constants import c, G, h

# Create a wavelength array (in mm):
waves = np.linspace(1.0, 3.0, 21)

"""
Explanation: Multi-Dimensional Arrays
Standard matrix notation is $A_{i,j}$, where i and j are
the row and column numbers, respectively.
Read $A_{i,j}$ as "A-sub-i-sub-j" or "A-sub-i-j".
Commas are often not used in the subscripts or
have different meanings.
In standard mathematics, the indexing starts with 1.
In Python, the indexing starts with 0.
Q. What is the rank of $A_{i,j,k,l}$?
The shape of an array is a $d$-vector (or 1-D array) that holds the number of elements in each dimension. $d$ represents the dimensionality of the array.
E.g., the shape of a $A_{i,j,k,l}$ is ($n_i$, $n_j$, $n_k$, $n_l$), where n denotes the number of elements in dimensions $i$, $j$, $k$, and $l$.
Two-Dimensional Numerical Python Arrays
A 2-D array is a matrix, and is analogous to an array of arrays, though each element of an array must have the same data type.

Example:  $$wave = \frac{c}{freq}$$

with wavelength in meters, 
c = 3.00e8 m/s, and
frequency in Hz.
We will convert wavelengths of 1 mm to 3 mm
to frequencies, bracketing the peak in the cosmic 
microwave background radiation.
End of explanation
"""


print(waves.max())
waves

# Now, convert to frequency 
# (note conversion from mm to m):

freqs = c / (waves / 1e3)
freqs

# Make a table & print (zip pairs up wave and freq 
# into a list of tuples):

table = [[wave, freq] for wave, freq in zip(waves, freqs)]

for row in table:
    print(row)

print(np.array(table))

# Just for review:

print(list(zip(waves, freqs)))

table = np.array([waves, freqs])
table

"""
Explanation: Q. What will the maximum (last element) of wave be? How to check?
End of explanation
"""


table.transpose()

# let's just work with the transpose

table = table.T 

"""
Explanation: Q. How could we regroup elements to match the previous incarnation? (row major)
End of explanation
"""


table.shape

"""
Explanation: Q. What should this yield?
End of explanation
"""


table[20][0]

table[20,0]

"""
Explanation: Q. What should this be?
End of explanation
"""


l = list(table)
print(l[20][0])
l[20,0]

table.shape

for index1 in range(table.shape[0]):

    # Q. What is table.shape[0]?
    
    for index2 in range(table.shape[1]):
        print('table[{}, {}] = {:g}'.format(index1, index2, 
                                            table[index1, index2]))
        
        # Q. What will this loop print?
        

"""
Explanation: Not possible for lists! :
End of explanation
"""


table.shape[0]

for row in table:   # don't be fooled, it's not my naming of the looper that does that!
    print(row)

for idontknowwhat in table:   
    print(idontknowwhat)

"""
Explanation: When you just loop over the elements of an array, you get rows:
End of explanation
"""


for index_tuple, value in np.ndenumerate(table):
    print('index {} has value {:.2e}'.format(index_tuple, value))

"""
Explanation: This could also be done with one loop using numpy's ndenumerate.
ndenumerate will enumerate the rows and columns of the array:
End of explanation
"""


print(table.shape)
print(type(table.shape))

"""
Explanation: Q. Reminder:  what is the shape of table?
End of explanation
"""


table.shape[0]

"""
Explanation: Q. So what is table.shape[0]?
End of explanation
"""


table.shape[1]

"""
Explanation: Q. And table.shape[1]?
End of explanation
"""


table[0]

"""
Explanation: Arrays can be sliced analogously to lists.
But we already saw, there's more indexing posssibilities on top with numpy.
End of explanation
"""


table[:, 0]

# Note that this is different.

# Q. What is this?

table[:][0]

# This will print the second column:

table[:, 1]

# To get the first five rows of the table:
    
print(table[:5, :])

print()

# Same as:
print(table[:5])

"""
Explanation: Q: How to get the first column instead?
End of explanation
"""


ndarray = np.zeros(2,3,4)    # will fail. Why? Hint: Look at error message

ndarray = np.zeros((2,3,4))

ndarray = np.arange(2*3*4).reshape((2,3,4))    # will fail. Why?

ndarray

ndarray[:, :, 0]

ndarray[..., 0]

"""
Explanation: Numpy also has a multi-dimensional lazy indexing trick under its sleeve:
End of explanation
"""


xArray1 = np.array([1, 2, 3], float)
xArray1

xArray1.T

xMatrix = np.matrix(xArray1)
print(type(xMatrix))
xMatrix

xMatrix.shape

xMatrix2 = xMatrix.transpose()
xMatrix2

# Or
xMatrix.T

"""
Explanation: Array Computing
For an array $A$ of any rank, $f(A)$ means applying the function
$f$ to each element of $A$.
Matrix Objects
End of explanation
"""


iMatrix = np.eye(3)  # or np.identity
iMatrix

# And
iMatrix2 = np.mat(iMatrix)  # 'mat' short for 'matrix'
iMatrix2

# Array multiplication.
# Reminder of xMatrix?
xMatrix

# Multiplication of any matrix by the identity matrix
# yields that matrix:
xMatrix * iMatrix

# Reminder of xMatrix2:
xMatrix2

xMatrix2 = iMatrix * xMatrix2
xMatrix2

xMatrix * xMatrix2

np.dot(xMatrix, xMatrix2)

xMatrix

xMatrix2

xArray  = np.array(xMatrix)
xArray2 = np.array(xMatrix2)
xArray * xArray2

xMatrix.shape, xMatrix2.shape

xArray.shape

np.array(xMatrix) * np.array(xMatrix2).T

"""
Explanation: Q. What is the identity matrix?
End of explanation
"""


