Numpy#

\(\newcommand{\re}{\mathbb{R}}\) Numpy provides many useful types and methods to perform numerical computations including vectors, matrices and linear algebra.

First import the numpy module.

import numpy

You only need to do this once, usually at the beginning of your code.

To access methods, etc. inside this module, use dot operator.

print(numpy.pi)
print(numpy.sin(numpy.pi/2))
3.141592653589793
1.0

It is common practice to import numpy like this.

import numpy as np

Note that np acts as an alias or short-hand for numpy.

print(np.pi)
print(np.sin(np.pi/2))
3.141592653589793
1.0

1-d arrays#

Create an array of zeros

x = np.zeros(5)
print(x)
[0. 0. 0. 0. 0.]

These one dimensional arrays are of type ndarray

type(x)
numpy.ndarray

Create an array of ones

x = np.ones(5)
print(x)
[1. 1. 1. 1. 1.]

Create and add two arrays

x = np.array([1.0, 2.0, 3.0])
y = np.array([4.0, 5.0, 6.0])
z = x + y
print(z)
[5. 7. 9.]

Get the size of array

print(len(x))
print(x.size)
3
3

Get the shape of an array

print(x.shape)
(3,)

We see that these are arrays of reals by default. We can specify the type

a = np.zeros(5, dtype=int)
print(a)
[0 0 0 0 0]

linspace#

This behaves same way as Matlab’s linspace function.

Generate 10 uniformly spaced numbers in [1,10]

x = np.linspace(1,10,10)
print(x)
[ 1.  2.  3.  4.  5.  6.  7.  8.  9. 10.]

Note that this includes the end points 1 and 10. The output of linspace is an ndarray of floats.

type(x)
numpy.ndarray

x = linspace(a,b,n) is such that x is an array of n elements

x[i] = a + i*h,   i=0,1,2,...,n-1,    h = (b-a)/(n-1)

so that

x[0] = a,   x[n-1] = x[-1] = b

Note that we get last element of array using x[-1]; similarly, the last but one is given by x[-2], etc.

arange#

x = np.arange(1,10)
print(x)
print(type(x))
[1 2 3 4 5 6 7 8 9]
<class 'numpy.ndarray'>

For \(m < n\), arange(m,n) returns

\[ m, m+1, \ldots, n-1 \]

Note the last value \(n\) is not included.

We can specify a step size

x = np.arange(1,10,2)
print(x)
[1 3 5 7 9]

If the arguments are float, the returned value is also float.

x = np.arange(1.0,10.0)
print(x)
[1. 2. 3. 4. 5. 6. 7. 8. 9.]
x = np.arange(0,1,0.1)
print(x)
[0.  0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9]

Beware of pitfalls - 1#

Create an array of ones.

x = np.ones(10)
print(x)
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]

Maybe we want set all elements to zero, so we might try this

x = 0.0
print(x)
0.0

x has changed from an array to a scalar !!! In Python, the type of a variable is determined by what we assign to it. The correct way to set zeroes is this.

x = np.ones(10)
print(x)
x[:] = 0.0
print(x)
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]

Beware of pitfalls - 2#

x = np.ones(5)
y = x
x[:] = 0.0
print('x = ', x)
print('y = ', y)
x =  [0. 0. 0. 0. 0.]
y =  [0. 0. 0. 0. 0.]

Why did y change ? This happened because when we do

y = x

then y is just a pointer to x, so that changing x changes y also. If we want y to be an independent copy of x then do this

x = np.ones(5)
y = x.copy()     # or y = np.copy(x)
x[:] = 0.0
print('x = ', x)
print('y = ', y)
x =  [0. 0. 0. 0. 0.]
y =  [1. 1. 1. 1. 1.]

2-d arrays#

2-d arrays can be considered as matrices, though Numpy has a separate matrix class.

Create an array of zeros

A = np.zeros((5,5))
print(A)
[[0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]]

The size must be specified as a tuple or list.

We can access the elements by two indices A[i,j]: here i is the row index and j is the column index.

 -----------j-------->
 |  [[0. 0. 0. 0. 0.]
 |   [0. 0. 0. 0. 0.]
 i   [0. 0. 0. 0. 0.]
 |   [0. 0. 0. 0. 0.]
 v   [0. 0. 0. 0. 0.]]

The top, left element is A[0,0].

We can modify the elements of an array.

A[1,1] = 1.0
print(A)
[[0. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]]

Create an array of ones

A = np.ones((2,3))
print(A)
[[1. 1. 1.]
 [1. 1. 1.]]

Create identity matrix

A = np.eye(5)
print(A)
[[1. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0.]
 [0. 0. 1. 0. 0.]
 [0. 0. 0. 1. 0.]
 [0. 0. 0. 0. 1.]]

Create an array by specifying its elements

A = np.array([[1.0, 2.0], 
              [3.0, 4.0]])
print(A)
[[1. 2.]
 [3. 4.]]

Create a random array and inspect its shape

m, n = 2, 3
A = np.random.rand(m,n)
print(A)
print('A.size =',A.size)
print('A.shape =',A.shape)
print('A.shape[0] =',A.shape[0],' (number of rows)')
print('A.shape[1] =',A.shape[1],' (number of columns)')
[[0.877971   0.01053046 0.847854  ]
 [0.89848359 0.22590672 0.84694337]]
A.size = 6
A.shape = (2, 3)
A.shape[0] = 2  (number of rows)
A.shape[1] = 3  (number of columns)

To print the elements of an array, we need two for loops, one over rows and another over the columns.

for i in range(m): # loop over rows
    for j in range(n): # loop over columns
        print(i,j,A[i,j])
0 0 0.8779709963198272
0 1 0.01053046308387895
0 2 0.8478540006662724
1 0 0.8984835888817763
1 1 0.22590671850775024
1 2 0.8469433724378825

To transpose a 2-d array

A = np.array([[1,2],[3,4]])
print("A ="); print(A)
B = A.T
print("B ="); print(B)
A =
[[1 2]
 [3 4]]
B =
[[1 3]
 [2 4]]

Diagonal matrix#

Create

\[\begin{split} A = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 2 & 0 & 0 \\ 0 & 0 & 3 & 0 \\ 0 & 0 & 0 & 4 \end{bmatrix} \end{split}\]
a = np.array([1,2,3,4])    # diagonal elements
A = np.diag(a)
print(A)
[[1 0 0 0]
 [0 2 0 0]
 [0 0 3 0]
 [0 0 0 4]]

Create \(n \times n\) tri-diagonal matrix

a = np.array([1,2,3])    # sub-diagonal  : length = n-1
b = np.array([4,5,6,7])  # main diagonal : length = n
c = np.array([-1,-2,-3]) # super-diagonal: length = n-1
A = np.diag(a,-1) + np.diag(b,0) + np.diag(c,+1)
print(A)
[[ 4 -1  0  0]
 [ 1  5 -2  0]
 [ 0  2  6 -3]
 [ 0  0  3  7]]

empty_like, etc.#

Sometimes we have an existing array and we want to create a new array of the same shape and type, but without initializing its elements.

A = np.random.rand(2,3)
B = np.empty_like(A)
print(A.shape,B.shape)
(2, 3) (2, 3)
C = np.zeros_like(A)
print(C)
[[0. 0. 0.]
 [0. 0. 0.]]
D = np.ones_like(A)
print(D)
[[1. 1. 1.]
 [1. 1. 1.]]

1-D array is neither row nor column vector#

x = np.array([1,2,3])
print(x.shape, x)
y = x.T
print(y.shape, y)
(3,) [1 2 3]
(3,) [1 2 3]

Create a row vector

x = np.array([[1,2,3]]) # row vector
print('x.shape =',x.shape)
print(x)
y = x.T
print('y.shape =',y.shape)
print(y)
x.shape = (1, 3)
[[1 2 3]]
y.shape = (3, 1)
[[1]
 [2]
 [3]]

Create a column vector

x = np.array([[1],
              [2],
              [3]]) # column vector
print('x.shape =',x.shape)
print(x)
y = x.T
print('y.shape =',y.shape)
print(y)
x.shape = (3, 1)
[[1]
 [2]
 [3]]
y.shape = (1, 3)
[[1 2 3]]

Functions like zeros and ones can return row/column vector rather than array.

x = np.ones((3,1)) # column vector
print('x ='); print(x)
y = np.ones((1,3)) # row vector
print('y ='); print(y)
x =
[[1.]
 [1.]
 [1.]]
y =
[[1. 1. 1.]]

Row/column vectors are like row/column matrix. We have to use two indices to access the elements of such row/column vectors.

Here we access elements of a column vector.

print(x[0][0])  # or x[0,0] 
print(x[1][0])
print(x[2][0])
1.0
1.0
1.0

x[0] gives an array for the first row.

print('First row of x       =',x[0], type(x[0]))
print('Element in first row =',x[0][0], type(x[0][0]))
First row of x       = [1.] <class 'numpy.ndarray'>
Element in first row = 1.0 <class 'numpy.float64'>

Accessing portions of arrays#

Array of 10 elements

x[0]

x[1]

x[2]

x[3]

x[4]

x[5]

x[6]

x[7]

x[8]

x[9]

0

1

2

3

4

5

6

7

8

9

x[-10]

x[-9]

x[-8]

x[-7]

x[-6]

x[-5]

x[-4]

x[-3]

x[-2]

x[-1]

x = np.linspace(0,9,10)
print(x)
[0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]

Get elements x[2],...,x[5]

print(x[2:6])
[2. 3. 4. 5.]

Hence x[m:n] gives the elements x[m],x[m+1],...,x[n-1].

Get elements from x[5] upto the last element

print(x[5:])
[5. 6. 7. 8. 9.]

Get the last element

print(x[-1])
9.0

Get element x[5] upto last but one element

print(x[5:-1])
[5. 6. 7. 8.]

Access every alternate element of array starting from beginning x[0]

print(x[0::2])
[0. 2. 4. 6. 8.]

and starting from x[1]

print(x[1::2])
[1. 3. 5. 7. 9.]

These operations work on multi dimensional arrays also.

A = np.random.rand(3,4)
print(A)
[[0.6158943  0.33880473 0.04845286 0.07684042]
 [0.35568814 0.30307182 0.33792877 0.2084261 ]
 [0.98275168 0.71575511 0.63949904 0.52737395]]
print(A[0,:]) # 0'th row
[0.6158943  0.33880473 0.04845286 0.07684042]
print(A[:,0]) # 0'th column
[0.6158943  0.35568814 0.98275168]
print(A[0:2,0:3]) # print submatrix
[[0.6158943  0.33880473 0.04845286]
 [0.35568814 0.30307182 0.33792877]]
A[0,:] = 0.0 # zero out zeroth row
print(A)
[[0.         0.         0.         0.        ]
 [0.35568814 0.30307182 0.33792877 0.2084261 ]
 [0.98275168 0.71575511 0.63949904 0.52737395]]

Arithmetic operations on arrays#

Some arithmetic operations act element-wise

x = np.array([1.0, 2.0, 3.0])
y = np.array([4.0, 5.0, 6.0])
print(x*y)  # multiply
[ 4. 10. 18.]
print(x/y)  # divide
[0.25 0.4  0.5 ]
print(y**x) # exponentiation
[  4.  25. 216.]
A = np.ones((3,3))
print(A*x)
[[1. 2. 3.]
 [1. 2. 3.]
 [1. 2. 3.]]

If A and x are arrays, then A*x does not give matrix-vector product. For that use dot

print(A.dot(x))
[6. 6. 6.]

or equivalently

print(np.dot(A,x))
[6. 6. 6.]

In Python3 versions, we can use @ to achieve matrix operations

print(A@x)
[6. 6. 6.]

We can of course do matrix-matrix products using dot or @

A = np.ones((3,3))
B = 2*A
print('A =\n',A)
print('B =\n',B)
print('A*B =\n',A@B)
A =
 [[1. 1. 1.]
 [1. 1. 1.]
 [1. 1. 1.]]
B =
 [[2. 2. 2.]
 [2. 2. 2.]
 [2. 2. 2.]]
A*B =
 [[6. 6. 6.]
 [6. 6. 6.]
 [6. 6. 6.]]

On vectors, @ performs dot product, i.e., x@y = dot(x,y)

x = np.array([1,1,1])
y = np.array([2,3,4])
print(x@y)
9

Some matrix/vector functions#

x = np.array([-3,-2,-1,0,1,2,3])
print('min     = ',np.min(x))
print('max     = ',np.max(x))
print('abs min = ',np.abs(x).min()) # np.min(np.abs(x))
print('abs max = ',np.abs(x).max()) # np.max(np.abs(x))
print('sum     = ',np.sum(x))
print('dot     = ',np.dot(x,x)) 
print('dot     = ',np.sum(x*x))
min     =  -3
max     =  3
abs min =  0
abs max =  3
sum     =  0
dot     =  28
dot     =  28

Note that np.sum(x*x) gives

\[ \sum_{i=0}^{n-1} x_i^2 \]

We can compute vector norms using numpy.linalg.norm (also see scipy.linalg.norm)

from numpy.linalg import norm
print(norm(x))        # L2 norm
print(norm(x,2))      # L2 norm
print(norm(x,1))      # L1 norm
print(norm(x,np.inf)) # Linf norm
5.291502622129181
5.291502622129181
12.0
3.0

or import the linalg module and use methods inside it.

import numpy.linalg as la
print(la.norm(x))        # L2 norm
print(la.norm(x,2))      # L2 norm
print(la.norm(x,1))      # L1 norm
print(la.norm(x,np.inf)) # Linf norm
5.291502622129181
5.291502622129181
12.0
3.0

These methods work on 2-d arrays treated as matrices.

A = 2*np.random.rand(3,3) - 1
print(A)
[[-0.67046385 -0.29285192  0.03569857]
 [-0.65840173  0.36651347  0.57328081]
 [-0.90564562  0.91828561  0.29756623]]
print('min       = ',np.min(A))
print('max       = ',np.max(A))
print('abs min   = ',np.abs(A).min()) # np.min(np.abs(x))
print('abs max   = ',np.abs(A).max()) # np.max(np.abs(x))
print('sum       = ',np.sum(A))
print('Frob norm = ',la.norm(A))        # Frobenius norm
print('L1 norm   = ',la.norm(A,1))      # L1 norm
print('L2 norm   = ',la.norm(A,2))      # L2 norm
print('Linf norm = ',la.norm(A,np.inf)) # Linf norm
min       =  -0.9056456216878264
max       =  0.9182856123523899
abs min   =  0.03569857041060853
abs max   =  0.9182856123523899
sum       =  -0.33601843643190565
Frob norm =  1.7846631019681387
L1 norm   =  2.234511206706409
L2 norm   =  1.6193085145520425
Linf norm =  2.121497465051808

Example: Matrix-vector product#

Let

\[ x \in \re^n, \qquad A \in \re^{m \times n}, \qquad y = A x \in \re^m \]

We can compute this element-wise

\[ y_i = \sum_{j=0}^{n-1} A_{ij} x_j, \qquad 0 \le i \le m-1 \]
m, n = 5, 10
x = np.random.rand(n)
A = np.random.rand(m,n)
y = np.zeros(m) # Important to set to zero
assert A.shape[1] == len(x) and len(y) == A.shape[0]
for i in range(m):
    for j in range(n):
        y[i] += A[i,j]*x[j]

We can verify that our result is correct by computing \(\| y - A x\|_\infty\)

print(np.linalg.norm(y-A@x, np.inf))
8.881784197001252e-16

We can also compute the product column-wise. Let

\[ A_{:,j} = \textrm{j'th column of A} \in \re^m \]

Then the matrix-vector product can also be written as

\[ y = \sum_{j=0}^{n-1} A_{:,j} x_j \]

Warning: This may have inefficient memory access since by default, numpy arrays have column-major ordering, see below.

y[:] = 0.0
for j in range(n):
    y += A[:,j]*x[j]

# Now check the result
print(np.linalg.norm(y-A@x, np.inf))
8.881784197001252e-16

Example: Matrix-Matrix product#

If \(A \in \re^{m\times n}\) and \(B \in \re^{n \times p}\) then \(C = AB \in \re^{m \times p}\) is given by

\[ C_{ij} = \sum_{k=0}^{n-1} A_{ik} B_{kj}, \qquad 0 \le i \le m-1, \quad 0 \le j \le p-1 \]
m,n,p = 10,8,6
A = np.random.rand(m,n)
B = np.random.rand(n,p)
C = np.zeros((m,p))
assert A.shape[1] == B.shape[0]
for i in range(m):
    for j in range(p):
        for k in range(n):
            C[i,j] += A[i,k]*B[k,j]

Let us verify the result is correct by computing

\[ |C-AB|_\infty = \max_{i,j} |C_{ij} - (AB)_{ij}| \]

using @ operator

print(np.abs(C - A@B).max())
4.440892098500626e-16

Another view-point is the following

\[ C_{ij} = (\textrm{i'th row of A}) \cdot (\textrm{j'th column of B}) \]
for i in range(m):
    for j in range(p):
        C[i,j] = A[i,:] @ B[:,j]

# Now check the result
print(np.abs(C - A@B).max())
0.0

Outer product of two vectors#

Given \(a \in \re^m\) and \(b \in \re^n\), both considered as column vectors, their outer product

\[\begin{split} A = a b^\top = \begin{bmatrix} | \\ a \\ | \end{bmatrix} \begin{bmatrix} - b^\top - \end{bmatrix} = \begin{bmatrix} | & | & & | \\ a b_0 & a b_1 & \ldots & a b_{n-1} \\ | & | & & | \end{bmatrix} \in \re^{m \times n} \end{split}\]

is an \(m \times n\) matrix with elements

\[ A_{ij} = a_i b_j, \qquad 0 \le i \le m-1, \quad 0 \le j \le n-1 \]
a = np.array([1,2,3])
b = np.array([1,2])
A = np.outer(a,b)
print('A.shape = ', A.shape)
print('A = \n', A)
A.shape =  (3, 2)
A = 
 [[1 2]
 [2 4]
 [3 6]]

Math functions#

Numpy provides standard functions like sin, cos, log, etc. which can act on arrays in an element-by-element manner. This is not the case for functions in math module, which can only take scalar arguments.

x = np.linspace(0.0, 2.0*np.pi, 5)
y = np.sin(x)
print('x =',x)
print('y =',y)
x = [0.         1.57079633 3.14159265 4.71238898 6.28318531]
y = [ 0.0000000e+00  1.0000000e+00  1.2246468e-16 -1.0000000e+00
 -2.4492936e-16]
A = np.random.rand(5,5) # 5x5 matrix
Y = np.sin(A)
print('A =\n',A)
print('Y =\n',Y)
A =
 [[0.22809643 0.56860732 0.56679492 0.13603414 0.28676897]
 [0.56896    0.41076214 0.46451547 0.27134507 0.35890288]
 [0.183774   0.12055133 0.84863557 0.13741146 0.05487704]
 [0.96001926 0.19785409 0.09602633 0.50222031 0.75816527]
 [0.05457281 0.85581373 0.80351782 0.67282891 0.71228897]]
Y =
 [[0.22612367 0.53845903 0.53693092 0.13561497 0.28285462]
 [0.53875618 0.39930819 0.44798966 0.26802754 0.35124723]
 [0.18274132 0.12025955 0.7503792  0.13697944 0.0548495 ]
 [0.81920261 0.19656574 0.09587882 0.48137286 0.68759041]
 [0.05454573 0.75510465 0.71980253 0.62320086 0.65356793]]

Memory ordering in arrays*#

By default, the ordering is same as in C/C++, the inner-most index is the fastest running one. For example, if we have an array of size (2,3), they are stored in memory in this order

a[0,0], a[0,1], a[0,2], a[1,0], a[1,1], a[1,2]

i.e.,

a[0,0] --> a[0,1] --> a[0,2]
                         |
   |----------<----------|
   |
a[1,0] --> a[1,1] --> a[1,2]
a = np.array([[1,2,3], [4,5,6]])
print('Row contiguous =', a[0,:].data.contiguous)
print('Col contiguous =', a[:,0].data.contiguous)
a.flags
Row contiguous = True
Col contiguous = False
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  WRITEBACKIFCOPY : False

To get fortran style ordering, where the outer-most index is the fastest running one, which corresponds to the following layout in memory

a[0,0], a[1,0], a[0,1], a[1,1], a[0,2], a[1,2]

i.e.,

a[0,0]    -- a[0,1]    -- a[0,2]
   |     |     |      |      |  
   v     ^     v      ^      v
   |     |     |      |      |
a[1,0] --    a[1,1] --    a[1,2]

create like this

b = np.array([[1,2,3], [4,5,6]], order='F')
print('Row contiguous =', b[0,:].data.contiguous)
print('Col contiguous =', b[:,0].data.contiguous)
b.flags
Row contiguous = False
Col contiguous = True
  C_CONTIGUOUS : False
  F_CONTIGUOUS : True
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  WRITEBACKIFCOPY : False

Tensor product array: meshgrid#

x = np.linspace(0,3,4)
y = np.linspace(0,2,3)
X, Y = np.meshgrid(x,y)
print('len(x) = ',len(x))
print('len(y) = ',len(y))
print('shape X= ',X.shape)
print('shape Y= ',Y.shape)
print('x = ', x)
print('y = ', y)
print('X = '); print(X)
print('Y = '); print(Y)
len(x) =  4
len(y) =  3
shape X=  (3, 4)
shape Y=  (3, 4)
x =  [0. 1. 2. 3.]
y =  [0. 1. 2.]
X = 
[[0. 1. 2. 3.]
 [0. 1. 2. 3.]
 [0. 1. 2. 3.]]
Y = 
[[0. 0. 0. 0.]
 [1. 1. 1. 1.]
 [2. 2. 2. 2.]]

If \(x \in \re^m\) and \(y \in \re^n\), then the output of meshgrid(x,y) is arranged like this

\[ X[i,j] = x[j], \qquad Y[i,j] = y[i], \qquad 0 \le i \le n-1, \quad 0 \le j \le m-1 \]

If we want the following arrangement

\[ X[i,j] = x[i], \qquad Y[i,j] = y[j], \qquad 0 \le i \le m-1, \quad 0 \le j \le n-1 \]

we have to do the following

X, Y = np.meshgrid(x,y,indexing='ij')
print('len(x) = ',len(x))
print('len(y) = ',len(y))
print('shape X= ',X.shape)
print('shape Y= ',Y.shape)
len(x) =  4
len(y) =  3
shape X=  (4, 3)
shape Y=  (4, 3)

The second form is useful when working with finite difference schemes on Cartesian grids, where we want to use i index running along x-axis and j index running along y-axis.

Reshaping arrays#

A = np.array([[1,2,3],
              [4,5,6]])
print(A)
[[1 2 3]
 [4 5 6]]
B = np.reshape(A,2*3,order='C')
print(B)
[1 2 3 4 5 6]
A1 = np.reshape(B,(2,3),order='C')
print(A1)
[[1 2 3]
 [4 5 6]]
C = np.reshape(A,2*3,order='F')
print(C)
[1 4 2 5 3 6]
A2 = np.reshape(C,(2,3),order='F')
print(A2)
[[1 2 3]
 [4 5 6]]

roll function: working with periodic arrays#

v = np.arange(1,11)
print(v)
[ 1  2  3  4  5  6  7  8  9 10]

Shift array by one index to left and wrap it from right side.

print(np.roll(v,-1))
[ 2  3  4  5  6  7  8  9 10  1]

Shift array by one index to right and wrap it from left side.

print(np.roll(v,1))
[10  1  2  3  4  5  6  7  8  9]

Example: Compute \(v'(x)\) for

\[ v(x) = \sin(x), \qquad x \in [0,2\pi] \]

using periodicity and central difference scheme

\[ w_j = \frac{v_{j+1} - v_{j-1}}{2h}, \qquad 0 \le j \le N-1 \]

on the grid

\[ x_j = j h, \qquad 0 \le j \le N-1, \qquad h = \frac{2\pi}{N} \]

Note that \(x_0 = 0\) and the last grid point \(x_{N-1} = 2\pi -h\). We should not include both \(x=0\) and \(x=2\pi\) in the grid.

N = 100
h = 2*np.pi/N
x = h * np.arange(N)
v = np.sin(x)
w = (np.roll(v,-1) - np.roll(v,1))/(2*h) # central finite difference
error = np.abs(w - np.cos(x)).max()
print("Error in FD = ", error)
Error in FD =  0.000657843760160759

Writing and reading files#

Write an array to file

A = np.random.rand(3,3)
print(A)
np.savetxt('A.txt',A)
[[0.62473935 0.60069034 0.71087167]
 [0.92393094 0.004749   0.28832257]
 [0.16404334 0.91473642 0.5024596 ]]

Check the contents of the file in your terminal

cat A.txt

We can also print it from within the notebook

!cat A.txt
6.247393536186006680e-01 6.006903383833456234e-01 7.108716689892817797e-01
9.239309350549314015e-01 4.748998255435177285e-03 2.883225740714975283e-01
1.640433412538903069e-01 9.147364198446430450e-01 5.024596009336028679e-01

Write two 1-D arrays as columns into file

x = np.array([1.0,2.0,3.0,4.0])
y = np.array([2.0,4.0,6.0,8.0])
np.savetxt('data.txt',np.column_stack([x,y]))
!cat data.txt
1.000000000000000000e+00 2.000000000000000000e+00
2.000000000000000000e+00 4.000000000000000000e+00
3.000000000000000000e+00 6.000000000000000000e+00
4.000000000000000000e+00 8.000000000000000000e+00

We can control the number of decimals saved, and use scientific notation

np.savetxt('data.txt',np.column_stack([x,y]),fmt='%8.4e')
!cat data.txt
1.0000e+00 2.0000e+00
2.0000e+00 4.0000e+00
3.0000e+00 6.0000e+00
4.0000e+00 8.0000e+00

We can read an existing file like this

d = np.loadtxt('data.txt')
print('Shape d =', d.shape)
x1 = d[:,0]
y1 = d[:,1]
print('x =',x1)
print('y =',y1)
Shape d = (4, 2)
x = [1. 2. 3. 4.]
y = [2. 4. 6. 8.]

Measuring time#

Loops are slow in Python and it is good to avoid them. The following example shows how to time code execution.

Create some random matrices

m = 1000
A = np.random.random((m,m))
B = np.random.random((m,m))

First we add two matrices with an explicit for loop.

%%timeit -r10 -n10
C = np.empty_like(A)
for i in range(m):
    for j in range(m):
        C[i,j] = A[i,j] + B[i,j]
228 ms ± 982 μs per loop (mean ± std. dev. of 10 runs, 10 loops each)

Now we use the + operator.

%%timeit -r10 -n10
C = A + B
789 μs ± 20.3 μs per loop (mean ± std. dev. of 10 runs, 10 loops each)

The loop version is significantly slower.