#
# ___Caution___: copying arrays in python is unique:
# In[2]:
a = [1,2,3]
b = a
# makes `b` alias to `a`. This means that
# In[3]:
b[1]=5
# also changes `a[1]` to 5:
# In[4]:
print(a,b)
# To make a full copy of an array, use the `copy` method:
# In[5]:
b = a # b and a are the same array
c = a.copy() # c and a are different arrays with the same contents
c[2] = 19
print(a,b,c)
# ## Operations on Lists
# Frequently we want to perform operations on lists
#
# Method | Description | Example
# :--- | :--- | :---
# `len` | Returns the number of entries in the list | `len(my_list)`
# `append(entry)` | Append an entry to the list | `my_list.append( 'another entry' )`
# `insert(index,entry)` | Insert an entry in the list | `my_list.insert(2,10)`
# `extend(list2)` | append a list | `my_list.append( ('a','b',x) )`
# `+` | append a list (same as `extend`) | `my_list + my_list`
# `remove(entry)` | Append an entry to the list | `my_list.remove( x )`
# `sort()` | sort the list - only works when the list is homogeneous | `x.sort()`
# `reverse()` | reverses the entries in the list | `my_list.reverse()`
# `pop(index)` | return and remove the element in the list at `index` | `my_list.pop(3)`
# `count(entry)` | return the number of occurences of `entry` in the list | `my_list.count(2)`
# `index(entry)` | return the index at which the first occurence of `entry` occurs | `my_list.index(x)`
# You can also use the keyword `in` with a list:
# In[6]:
fruits = ['apple','pear','banana','payapa']
if 'banana' in fruits:
print('Yes!')
else:
print('No!')
# ## Indexing and Slicing Lists
#
# Indexing a list is done with the `[]` operator, and is 0-based (0 indicates the first entry in the list). For example, given:
# In[7]:
fruits = ['apple','banana','grapefruit','orange']
# the following table shows various indexing operations:
#
# Operation | Result | Description
# :--- | :--- | :---
# `fruits[1]` | `banana` | Accesses the second element in the list
# `fruits[-1]` | `orange` | Accesses the last element in the list
# `fruits[-2]` | `grapefruit` | Accesses the second to last element in the list
# `fruits[:2]` | `[apple,banana]` | The first two elements in the list
# `fruits[1:3]`| `[banana,grapefruit]` | The second and third elements in the list
#
# The `:` operator allows us to perform _slicing_, accessing a subset of the entries in a list.
# You can also use slicing to replace elements of a list. For example:
# In[8]:
x = [1,2,3,4]
y = x[1:3] # note that this doesn't make a copy! y is refering to part of x
y.reverse()
print("y: ", y)
x[0:2] = y
print("x: ", x)
# ## Loops and Lists
# There are a few ways to loop over lists. This is perhaps best illustrated by a few examples.
# ### Iterating a list
# In[9]:
fruits = ['apple','banana','orange']
for i in fruits:
print(i)
# Here, `i` is an _iterator_ that represents each entry in the list
# ### Index loops
# Here we use the `range()` function, which builds a range space for the loop. This allows us to use `i` as an index:
# In[10]:
fruits = ['apple','banana','orange']
for i in range(0,len(fruits)):
print('Fruit {} = {}'.format(i,fruits[i]))
#
#
# `range(lo,hi)` creates a range of integers from __`lo`__ to __`hi-1`__
# ### List comprehensions
# List comprehensions can be used to quickly build lists conforming to specific patterns. For example, if we wanted to build a list
# $$x_i=i^2, \quad i=1\ldots4$$
# we can do this by:
# In[11]:
x = [i**2 for i in range(1,4)]
print(x)
# Similarly, to achieve $y_i=2^i, \; i=1\ldots 8$, we can do:
# In[12]:
y = [2**i for i in range(1,9)]
print(y)
# List comprehensions provide a relatively simple syntax to build these types of lists.
# # Tuples
# In python, a _tuple_ is like a list, but has a few differences:
# * It is declared using `()` rather than `[]`
# * It is immutable - it cannot be changed once it is built
#
# Elements in a tuple are accessed in the same way as lists, using the `[]` operator.
#
# Generally, you will use lists rather than tuples.
# ---
# # Numpy Arrays
# The list functionality in python is not as useful as it could be when it comes to numerical operations. For example, you cannot perform mathematical operations on lists.
# This is where [numpy](https://docs.scipy.org/doc/numpy/reference/) comes in.
#
# Unlike a Python list, a Numpy arrays is _homogeneous_ - they contain entries of the same type (e.g., integer, real, complex).
#
# Here and below, we will assume that you have:
# In[13]:
import numpy as np
# so that we can use __`np.`__ to shorten reference to numpy functions.
# ## Constructing Numpy Arrays
# A numpy array is characterized by its _shape_ and the type of elements it contains.
# In[14]:
x = np.array( [1,2,3] ) # a 1-dimensional row vector
y = np.array( [ [1,2,3],[4,5,6] ] ) # a 2-dimensional matrix
print(x.shape,x)
print(y.shape,y)
# ### Data Types
# You can explicitly specify the [type of the data](https://docs.scipy.org/doc/numpy-1.12.0/reference/arrays.dtypes.html#specifying-and-constructing-data-types) in the array. Here are some of the common types you will use:
#
# Keyword | Description
# :---|:---
# [int](https://docs.python.org/dev/library/functions.html#int) | Integer
# [bool](https://docs.python.org/dev/library/functions.html#bool) | boolean (True/False)
# [float](https://docs.python.org/dev/library/functions.html#float) | Floating point (real)
# [complex](https://docs.python.org/dev/library/functions.html#complex) | Complex numbers
# [string](https://docs.python.org/dev/library/stdtypes.html#str) | string
# (other) | User-defined data types
# #### Example
# In[15]:
x = np.array( [1,2,3], dtype=complex )
print(x) ## [ 1.+0.j 2.+0.j 3.+0.j ]
# ### Arrays over specified ranges
# Function | Description
# :--- | :---
# `linspace(lo,hi,npts)` | Builds a 1D array with a `npts` entries between `lo` and `hi`
# `arange(lo,hi,spacing)` | Builds a 1D array spaced with `spacing` starting at `lo` and ending near `hi`
# `logspace(lo,hi,npts)` | Builds a 1D array with `npts` points between $10^\mathrm{lo}$ and $10^\mathrm{hi}$
# [`meshgrid(x,y,...)`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.meshgrid.html#numpy.meshgrid) | Given vectors specifying the range of each axis, builds a grid.
# ### Other Constructors
#
# Command | Description
# :--- | :---
# `empty(shape)` | Build an empty array.
# `empty_like(a)` | Build an empty array shaped like `a`
# `eye(N)` | Build a 2D identity matrix with `N` rows and columns
# `ones(shape)` | build an array of ones with the specified `shape`
# `ones_like(a)` | Build an array of ones shaped like `a`
# `zeros(shape)` | build an array of zeros with the specified `shape`
# `zeros_like(a)` | Build an array of zeros shaped like `a`
# `full(shape,val)` | Build an array of the specified shape filled with `val`
# `full_like(a,val)` | Build an array shaped like `a` filled with `val`
# `random.random(shape)` | Build an array of random numbers with the specified shape
#
# Examples:
# In[16]:
x = np.ones( [1,3] ) # 3-element row vector
y = np.empty_like( x ) # empty 3-element row vector
z = np.zeros( [3,3] ) # 3x3 matrix
p = np.full_like(z,np.pi) # 3x3 matrix full of 𝜋
r = np.random.random([2,3])
# All of these can have an additional `dypte` argument to specify the type of array to build (see [above](#Data-Types)).
#
#
# For more information on many other ways of building arrays, see the [numpy docs](https://docs.scipy.org/doc/numpy/reference/routines.array-creation.html)
# ## Numpy Matrices
#
# Most of the functions mentioned [above](#Other-Constructors) such as `zeros`, `ones`, `full`, `eye`, etc. will create matrices as well - just give the appropriate shape.
#
# Additionally, the [diag](https://docs.scipy.org/doc/numpy/reference/generated/numpy.diag.html#numpy.diag) function is very useful:
# * `diag(v,k)` builds a matrix with `v` on its `k`th diagonal. For example, the following builds a tridiagonal matrix:
# In[17]:
n = 5
d = np.full(n,-3)
ud = np.full(n-1,1)
mat = np.diag(d,0) + np.diag(ud,1) + np.diag(ud,-1)
# * `diag(m,k)` extracts the `k`th diagonal of the matrix `m`
# In[18]:
m = np.random.random([5,5])
# extract the main diagonal of m
md = np.diag(m,0)
# ## Manipulating Numpy Arrays
# There are many [array manipulation tools](https://docs.scipy.org/doc/numpy/reference/routines.array-manipulation.html#array-manipulation-routines). Some of the more frequently used ones include:
#
# Function | Description
# :--- | :---
# [`reshape(a,shape)`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.reshape.html#numpy.reshape) | Reshape the data in `a` to a `shape`
# [`ndarray.flat`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.flat.html#numpy.ndarray.flat) | Obtain an iterator over the array
# [`transpose(a)`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.transpose.html#numpy.transpose) | Transposes the array
# [`tile(a,nrep)`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.tile.html#numpy.tile) | Tile `a` `nrep` times. `nrep` can be an array.
# [`flip(a,axis)`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.flip.html#numpy.flip) | Reverse the elements along the `axis` dimension of `a`.
# [`fliplr(a)`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.fliplr.html#numpy.fliplr) | Flip the array in the left/right direction
# [`flipud(a)`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.flipud.html#numpy.flipud) | Flip the array in the up/down direction.
# ### Indexing and Slicing
# Numpy arrays are [indexed](https://docs.scipy.org/doc/numpy/reference/arrays.indexing.html) just like python lists:
# In[19]:
a = np.linspace(0,4,10)
a[3] = -2
for i in a:
print(i)
# You can also slice arrays:
# In[20]:
a = np.random.random([4,4])
print(a)
a[1:2,1:3]
x = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
print(x[1:7:2])
# And slice from the "back" of arrays:
# In[21]:
a = np.arange(0,10,1)
print(a[-3:4:-1]) # start at third-to last, end at fifth
# And slice all after:
# In[22]:
a = np.arange(0,10,1)
print(a[5:])
# # Mathematical Operations on Numpy Arrays
#
# Numpy arrays support typical mathematical operations like `+`, `-`, `*` (element-wise multipy), `/` (element-wise divide) and `**` (element-wise exponentiation) provided the arrays are the same shape.
#
# Numpy provides numerous [mathematical functions](https://docs.scipy.org/doc/numpy/reference/routines.math.html) that operate on arrays. For example:
# In[23]:
x = np.linspace(-np.pi,np.pi)
y = np.sin(x)
z = x**2
# In[24]:
n = 10
np.prod( np.arange(1,n+1,1) ) # the factorial of n
# ## Array Multiplication
#
# Given the arrays
# $A = \left[\begin{array}{cc}
# 1 & 2\\
# 3 & 4
# \end{array}\right]$,
# $b=\left(\begin{array}{c} 5\\6\end{array}\right)$,
# and
# $c=\left(\begin{array}{c} 7\\8\end{array}\right)$,
# In[25]:
A = np.array([[1,2],[3,4]])
b = np.array([[5],[6]])
c = np.array([[7],[8]])
# ### Elemental Multiplication
# For those coming from Matlab, the most familiar application of elemental multiplication is on arrays of the same shape.
# However, elemental multiplication does not require arrays to have the same shape. For example:
#
# Operation | Description | Result
# :-------- | :---------- | :-----
# `A * A` | Square elements in `A` | $\begin{array}{cc} 1&4\\9&16\end{array}$
# `b * c` | Element-wise multiplication of `b` and `c` | $\begin{array}{c} 35\\48\end{array}$
# `A * b` | Multiply a 2x2 by a 2x1. Results in a 2x2 | $\begin{array}{cc} 5&10\\18&24\end{array}$
# `b.transpose() * c` | Element-Multiply a row vector to a column vector | $\begin{array}{cc} 35&42\\40&48\end{array}$
#
# ### Matrix Multiplication
# While `*` implies _elemental_ multiplication, `@` implies _matrix_ multiplication.
#
# Here are some example operations on the above arrays:
#
# Operation | Result
# :--- | :---
# `A @ b` | $\begin{array}{c} 17 \\ 39 \end{array}$
# `b.transpose() @ c` | 39
# `b@c` | error
# `b.transpose() @ A @ c` | 433
# ## More useful stuff
# Among the other things that you should be aware of:
# * Fourier Transform through the [`numpy.fft`](https://docs.scipy.org/doc/numpy/reference/routines.fft.html) module.
# * Linear algebra through the [`numpy.linalg`](https://docs.scipy.org/doc/numpy/reference/routines.linalg.html) module. This includes things like dot products, matrix products, norms, matrix decomositions, etc.
# * [Statistics](https://docs.scipy.org/doc/numpy/reference/routines.statistics.html).
# * [I/O](https://docs.scipy.org/doc/numpy/reference/routines.io.html) to help with reading/writing arrays from/to disk.
# * Advanced [indexing tools](https://docs.scipy.org/doc/numpy/reference/routines.indexing.html).
#
# # Masking Numpy Arrays
# To do... See docs [here](https://docs.scipy.org/doc/numpy/reference/maskedarray.html)