Part 1. Numpy for numerical computations#

1.1 Introduction to Numpy#

Numpy is a powerful library for numerical computations in Python. It provides support for arrays, matrices, and a wide range of mathematical functions to operate on these data structures. The full official documentation for Numpy can be found here.

1.2 Numpy Objects: N-dimensional Arrays#

Numpy uses objects called arrays. They are similar to Python lists but offer more functionality and are more efficient for numerical operations. They are also significantly faster to use in computations than Python lists. The speed-up is that numpy arrays must contain elements of the same data type (homogeneous data structures), as opposed to python lists which can contain multiple different data types (heterogeneous data structures).

Most NumPy arrays have some restrictions. For instance:

  • All elements of the array must be of the same type of data.

  • The shape must be “rectangular”, not “jagged”; e.g., each row of a two-dimensional array must have the same number of columns.

We will use the word “array” to refer to an instance of ndarray.

Then, we can create and manipulate arrays of any dimension by using the np.array() function:

Syntax: arr = np.array(data, dtype)

import numpy as np  

# Creating a 1D array from a list
arr1d = np.array([1, 2, 3, 4, 5])  
print("\nArray 1D:\n ", arr1d)    

# Creating a 2D array from a list of lists
arr2d = np.array([[1, 2, 3], [4, 5, 6]])  
print("\nArray 2D:\n",arr2d)

# Creating a 2D array from a list of lists with a specified data type
arr2d = np.array([[1, 2, 3], [4, 5, 6]], dtype=float)  
print("\nArray 2D:\n", arr2d)

NumPy also supports 0-dimensional arrays, however they are not very useful.

# Creating a 0D array
arr0d = np.array(42)  
print(arr0d)
print(arr0d.ndim) 

Short exercise: Create a 1D array, a 2D array, and a 3D array with different data types. 1D array with integers, 2D array with floats, and 3D array with complex numbers.

In some cases it can be useful to create arrays with specific type of floating point numbers. Numpy supports several floating point types, each with different precision and memory requirements. The most commonly data types in Numpy are:

Data Type

Description

int8

8-bit signed integer

int16

16-bit signed integer

int32

32-bit signed integer

int64

64-bit signed integer

float16

Half precision float: sign bit, 5 bits exponent, 10 bits fraction

float32

Single precision float: sign bit, 8 bits exponent, 23 bits fraction

float64

Double precision float: sign bit, 11 bits exponent, 52 bits fraction

1.2.1 Array Attributes#

The most important attributes of a Numpy array are:

  • ndarray.ndim: Returns the number of dimensions (axes) of the array.

  • ndarray.shape: Returns a tuple representing the dimensions of the array.

  • ndarray.size: Returns the total number of elements in the array.

  • ndarray.dtype: Returns the data type of the elements in the array.

Short exercise: From the previous exercise, print the attributes of the 1D, 2D and 3D array you created.

1.2.2 Array Indexing and Slicing#

There are some very useful methods to access and manage elements in a Numpy arrays. Some of the most commonly used techniques are:

  • Indexing: You can access individual elements of an array using their indices. Remember that Numpy uses zero-based indexing, so the first element has an index of 0. Indexes always are enclosed in square brackets [].

General syntax for indexing:

arr[i]          # 1D array
arr[i, j]       # 2D array
arr[i, j, k]    # 3D array, where i, j, k are the indices of the elements you want to access.

Short exercise: From the previous arrays you created, access the first element of the 1D array, the element in the first row and second column of the 2D array, and the element in the second block, first row, and second column of the 3D array.

  • Slicing: You can extract subarrays using slicing. For example, arr[1:4] extracts elements from index 1 to 3 in a 1D array, and arr[0:2, 1:3] extracts a subarray from the first two rows and columns 1 to 2 in a 2D array.

General syntax for slicing:

arr[start:stop:step]          # 1D array
arr[start_row:stop_row, start_col:stop_col]  # 2D array
arr[start_block:stop_block, start_row:stop_row, start_col:stop_col]  # 3D array

Short exercise: From the previous arrays you created, slice the first three elements of the 1D array, the first two rows and first two columns of the 2D array, and the first block, first two rows, and first two columns of the 3D array.

  • Boolean Indexing: We can also use boolean arrays to index another array to match conditions. For example, arr[arr > 5] returns all elements in arr that are greater than 5.

Short exercise: From any of previous arrays, use boolean indexing to get all elements greater than an arbitrary value.

  • Fancy Indexing: You can use arrays of indices to access multiple elements at once. For example, arr[[0, 2, 4]] returns the elements at indices 0, 2, and 4.

Short exercise: Try creating a 1D array and using fancy indexing to access first, third, and fourth elements.

1.2.3 Array Creation Functions#

Numpy provides several functions to create arrays with specific values or patterns. Some of the most commonly used functions are:

Function

Description

np.array(object, dtype=None)

Creates an array from an object (e.g., list, tuple).

np.zeros(shape, dtype=float)

Creates an array filled with zeros.

np.ones(shape, dtype=float)

Creates an array filled with ones.

np.full(shape, fill_value, dtype=None)

Creates an array filled with a specified value.

np.eye(N, M=None, k=0, dtype=float)

Creates an identity matrix.

np.arange(start, stop, step, dtype=None)

Creates an array with evenly spaced values within a given interval.

np.linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None)

Creates an array with a specified number of evenly spaced values between two endpoints.

np.random.rand(d0, d1, ..., dn)

Creates an array with random values from a uniform distribution over [0, 1).

np.random.seed(seed)

Sets the seed for the random number generator to ensure reproducibility.

np.asarray(a, dtype=None)

Converts an input to a Numpy array.

Short exercise: With the methods above, create a 3x3 identity matrix, a 2x4 array filled with ones, and a 1D array with 10 evenly spaced values between 0 and 1.

1.2.4 Array Operations#

Numpy supports a wide range of operations on arrays, including arithmetic operations, statistical functions, and linear algebra operations. Some common operations include:

Arithmetic operations:#

You can perform any arithmetic operations on arrays of the same shape.

Operation

Description

Example

Addition (+)

Adds corresponding elements of two arrays.

arr1 + arr2

Subtraction (-)

Subtracts corresponding elements of two arrays.

arr1 - arr2

Multiplication (*)

Multiplies corresponding elements of two arrays.

arr1 * arr2

Division (/)

Divides corresponding elements of two arrays.

arr1 / arr2

Floor Division (//)

Performs floor division on corresponding elements of two arrays.

arr1 // arr2

Modulus (%)

Computes the modulus of corresponding elements of two arrays.

arr1 % arr2

Exponentiation (**)

Raises elements of the first array to the power of corresponding elements of the second array.

arr1 ** arr2

Short exercise using arithmetic operations: Create two 2D arrays of the same shape and perform addition, subtraction, multiplication, and division on them.

Statistical functions#

Numpy provides functions to compute statistics such as mean, median, standard deviation, variance, etc.

Function

Description

Example

np.mean(arr, axis=None)

Computes the mean of the array elements along the specified axis.

np.mean(arr)

np.median(arr, axis=None)

Computes the median of the array elements along the specified axis.

np.median(arr)

np.std(arr, axis=None)

Computes the standard deviation of the array elements along the specified axis.

np.std(arr)

np.var(arr, axis=None)

Computes the variance of the array elements along the specified axis.

np.var(arr)

np.min(arr, axis=None)

Returns the minimum value in the array along the specified axis.

np.min(arr)

np.max(arr, axis=None)

Returns the maximum value in the array along the specified axis.

np.max(arr)

np.sum(arr, axis=None)

Computes the sum of the array elements along the specified axis.

Linear algebra operations#

Numpy includes functions for matrix multiplication, dot products, eigenvalues, eigenvectors, etc.

Function

Description

Example

np.dot(a, b)

Computes the dot product of two arrays.

np.dot(arr1, arr2)

np.matmul(a, b)

Performs matrix multiplication of two arrays.

np.matmul(arr1, arr2)

np.linalg.inv(a)

Computes the inverse of a square matrix.

np.linalg.inv(matrix)

np.linalg.det(a)

Computes the determinant of a square matrix.

np.linalg.det(matrix)

np.linalg.eig(a)

Computes the eigenvalues and right eigenvectors of a square matrix.

np.linalg.eig(matrix)

np.linalg.svd(a)

Performs Singular Value Decomposition (SVD) on a matrix.

np.linalg.svd(matrix)

Short exercise: Create a 2x2 matrix and compute its inverse, determinant, and eigenvalues.

Reshaping and Transposing#

We can manipulate shapes of an array using functions like reshape() and transpose().

Function

Description

Example

ndarray.reshape(new_shape)

Reshapes the array to the specified new shape.

arr.reshape((2, 3))

ndarray.T

Returns the transpose of the array.

arr.T

ndarray.flatten()

Flattens a multi-dimensional array into a 1D array.

ndarray.ravel()

Returns a flattened array (1D) without making a copy if possible.

arr.ravel()

ndarray.resize(new_shape)

Resizes the array to the specified new shape.

arr.resize((3, 2))

Stacking and Splitting#

You can combine or split arrays using functions like hstack(), vstack(), split(), etc.

Function

Description

Example

np.hstack(tup)

Stacks arrays in sequence horizontally (column-wise).

np.hstack((arr1, arr2))

np.vstack(tup)

Stacks arrays in sequence vertically (row-wise).

np.vstack((arr1, arr2))

np.concatenate((a1, a2, ...), axis=0)

Concatenates a sequence of arrays along the specified axis.

np.concatenate((arr1, arr2), axis=0)

np.split(ary, indices_or_sections, axis=0)

Splits an array into multiple sub-arrays along the specified axis.

np.split(arr, 3)

np.hsplit(ary, indices_or_sections)

Splits an array into multiple sub-arrays horizontally (column-wise).

np.hsplit(arr, 2)

np.vsplit(ary, indices_or_sections)

Splits an array into multiple sub-arrays vertically (row-wise).

np.vsplit(arr, 2)

Short exercise: Create two 2D arrays and stack them both horizontally and vertically. Then, split one of the stacked arrays into two sub-arrays.

Sorting and Searching#

Numpy provides functions to sort arrays and search for specific values.

Function

Description

Example

np.sort(arr, axis=-1)

Returns a sorted copy of the array along the specified axis.

np.sort(arr)

ndarray.argsort(axis=-1)

Returns the indices that would sort the array along the specified axis.

arr.argsort()

np.where(condition, x, y)

Returns elements chosen from x or y depending on the condition.

np.where(arr > 0, arr, 0)

Short exercise: Create a 1D array with random integers, sort it, and find the indices that would sort the array. Then, use np.where to create a new array where all negative values are replaced with zero.

Input and Output#

Numpy provides functions to automatically read from and write to files, both in text and binary formats. Note: it only reads and writes numerical data. If you want to read and write text data, you can use Python’s built-in functions or Pandas library (not explored in this course).

Function

Description

Example

np.loadtxt(fname, dtype=float, delimiter=None)

Loads data from a text file.

np.loadtxt('data.txt')

np.savetxt(fname, X, fmt='%.18e', delimiter=' ')

Saves an array to a text file.

np.savetxt('data.txt', arr)

np.load(fname, mmap_mode=None)

Loads data from a binary file in NumPy .npy format.

np.load('data.npy')

np.save(fname, arr, allow_pickle=True)

Saves an array to a binary file in NumPy .npy format.

np.save('data.npy', arr)

Example of saving and loading an array to/from a text file:

# Creating an array
arr = np.array([[1, 2, 3], [4, 5, 6]], dtype=float)
arr
# Saving the array to a text file
np.savetxt('data.txt', arr, fmt='%.2f', delimiter=',', header='Column1,Column2', comments='', )#footer='End of data')  

# Loading the array from the text file
loaded_arr = np.loadtxt('data.txt', delimiter=',', skiprows=1, encoding='utf-8')  # Skip the header row, and use utf-8 encoding

print("Loaded Array from text file:\n", loaded_arr)

Both np.savetxt and np.loadtxt have many parameters to customize the saving and loading process, including headers, delimiters, and data types. You can check:

Short exercise: Create a 2D array, save it to a text file, and then load it back into a new array. Print both arrays to verify they are the same.

Copying and Masking#

You can copy arrays and create masks to filter or modify elements in an array based on certain conditions.

Function

Description

Example

np.copy(arr)

Creates a copy of the array.

arr_copy = np.copy(arr)

np.ma.masked_array(data, mask=None)

Creates a masked array, where certain elements are marked as invalid or missing.

np.ma.masked_array(arr, mask)

np.ma.masked_where(condition, arr)

Masks elements of the array where the condition is True.

np.ma.masked_where(arr < 0, arr)

np.ma.masked_equal(arr, value)

Masks elements of the array that are equal to a specified value.

np.ma.masked_equal(arr, 0)

Another method to copy an array can be just assigning the array to a new variable.

# Creating a copy of an array using ndarray.copy()
arr_new = arr_init.copy()
# Creating a copy of an array using assignment
arr_new = arr_init

Short exercise: Create a 1D array, make a copy of it, and then create a masked array where all elements less than a certain value are masked.

Important methods for working with Matrices and Vectors#

Function

Description

Example

np.trace(a, offset=0, axis1=0, axis2=1, dtype=None, out=None)

Returns the sum of the diagonal elements of a 2D array (matrix).

np.trace(matrix)

np.diagonal(a, offset=0, axis1=0, axis2=1)

Returns the specified diagonal of a 2D array (matrix).

np.diagonal(matrix)

np.cross(a, b, axisa=-1, axisb=-1, axisc=-1, axis=None)

Computes the cross product of two vectors.

np.cross(vec1, vec2)

np.linalg.norm(x, ord=None, axis=None, keepdims=False)

Computes the norm (magnitude) of a vector or matrix.

np.linalg.norm(vec)

np.linalg.solve(a, b)

Solves a system of linear equations Ax = b.

np.linalg.solve(A, b)

np.matmul(a, b)

Performs matrix multiplication of two arrays.

np.matmul(A, B)

np.dot(a, b)

Computes the dot product of two arrays.

np.dot(A, B)

Short exercise: Create a 2x2 matrix and a 2D vector, then compute the trace of the matrix, the diagonal elements, the cross product of the vector with itself, and the norm of the vector.

Numpy constants and trigonometric functions#

Numpy provides several mathematical constants that can be useful in numerical computations.

Constant

Description

Value

np.pi

The mathematical constant π (pi).

3.141592653589793

np.e

The mathematical constant e (Euler’s number).

np.inf

Represents positive infinity.

inf

np.nan

Represents “Not a Number” (NaN).

nan

Numpy also provides a variety of trigonometric functions that operate on arrays element-wise.

Function

Description

Example

np.sin(x)

Computes the sine of x (x in radians).

np.sin(np.pi/2)

np.cos(x)

Computes the cosine of x (x in radians).

np.cos(0)

np.tan(x)

Computes the tangent of x (x in radians).

np.tan(np.pi/4)

np.arcsin(x)

Computes the inverse sine (arcsine) of x.

np.arcsin(1)

np.arccos(x)

Computes the inverse cosine (arccosine) of x.

np.arccos(0)

np.arctan(x)

Computes the inverse tangent (arctangent) of x.

np.arctan(1)

np.degrees(x)

Converts angles from radians to degrees.

np.degrees(np.pi)

np.radians(x)

Converts angles from degrees to radians.

np.radians(180)

1.3 Non-numeric data types in Numpy#

Despite being primarily designed for numerical data, Numpy also supports non-numeric data types, such as strings and booleans. Here are some examples of how to create and manipulate arrays with these data types:

import numpy as np
# Creating a 1D array of strings
str_arr =  np.array(['a','bcd'])
print(str_arr)
print(str_arr.dtype)

arr = np.array(['a','bcd','efghijk'])
print(arr)
print(arr.dtype)

# Creating a 1D array of booleans
bool_arr = np.array([True, False, True])
print(bool_arr)
print(bool_arr.dtype)

Boolean arrays can be very useful for indexing and filtering data in Numpy. For example, a boolean array to select elements from another array that meet a certain condition.

Short exercise: Use boolean arrays to represent the state of qubits (e.g., True for |1⟩ and False for |0⟩) and perform operations based on their states.

1.4 Linear algebra with Numpy#

Numpy provides a comprehensive set of functions for performing linear algebra operations.

1.4.1 Scalar Array/Matrix Operations#

You can perform scalar operations on arrays and matrices, such as addition, subtraction, multiplication, and division. These operations are applied element-wise.

import numpy as np
# Creating a 2D array (matrix)
A = np.array([[1,2],[3,4]], dtype='float')

# Displaying the original matrix
print(A, '')

# Performing scalar operations
print(2*A,'')

# Element-wise exponentiation
print(A**2,'')

# Element-wise division
print(A/2,'')

# Element-wise floor division
print(A//2,'')

# Element-wise addition
print(A+2,'')

# Element-wise subtraction
print(A-2,'')

# Element-wise modulus
print(A%2,'')

1.4.2 Array-Array Operations#

As we have seen, you can perform element-wise operations between two arrays of the same shape.

import numpy as np
# Creating two 2D arrays (matrices) of the same shape
arr1 = np.array([1,2,3,4,5])
arr2 = np.array([6,7,8,9,10], dtype='float')

print('arr1 + arr2 = ', arr1+arr2)
print('arr1 - arr2 = ', arr1-arr2)
print('arr1 * arr2 = ', arr1*arr2)
print('arr1 / arr2 = ', arr1/arr2)
print('arr1 ** arr2 = ', arr1**arr2)
print('arr1 // arr2 = ', arr1//arr2)
print('arr1 % arr2 = ', arr1%arr2)

While the above arrays are vectors we can perform the same operations higher-dimensional arrays (matrices) in the same way.

import numpy as np
# Creating two 2D arrays (matrices) of the same shape
A = np.array([[1,2],[3,4]], dtype='float')
B = np.array([[5,6],[7,8]], dtype='float')

print('A + B = ', A + B)
print('A - B = ', A - B)
print('A * B = ', A * B)
print('A / B = ', A / B)
print('A ** B = ', A ** B)
print('A // B = ', A // B)
print('A % B = ', A % B)

Short exercise: Create two 3x3 matrices and perform element-wise addition, subtraction, multiplication, and division on them.

1.4.3 Matrix Multiplication and Dot Product#

For matrix multiplication, you can use the @ operator or the np.matmul() function.

import numpy as np
# Creating two 2D arrays (matrices)
A = np.array([[1,2],[3,4]], dtype='float')
B = np.array([[5,6],[7,8]], dtype='float')

# Performing matrix multiplication using the @ operator
C = A @ B
print('Matrix multiplication using @ operator:', C)

# Performing matrix multiplication using np.matmul()
D = np.matmul(A, B)
print('Matrix multiplication using np.matmul():', D)

You can also compute the dot product of two vectors using the np.dot() function or the @ operator.

import numpy as np
# Creating two 1D arrays (vectors)
vec1 = np.array([1, 2, 3], dtype='float')
vec2 = np.array([4, 5, 6], dtype='float')

# Computing the dot product using np.dot()
dot_product1 = np.dot(vec1, vec2)
print('Dot product using np.dot():', dot_product1)

# Computing the dot product using the @ operator
dot_product2 = vec1 @ vec2
print('Dot product using @ operator:', dot_product2)

Short exercise: Create two 2x2 matrices and compute their matrix product using both the @ operator and the np.matmul() function. Then, create two 1D arrays (vectors) and compute their dot product using both the np.dot() function and the @ operator.

1.4.4 Other Linear Algebra Operations#

Numpy provides several other linear algebra operations, such as computing the transpose, inverse, determinant, and eigenvalues/eigenvectors of matrices.

import numpy as np
# Creating a 2D array (matrix)
A = np.array([[1,2],[3,4]], dtype='float')

# Computing the transpose of the matrix
A_T = A.T
print('Transpose of A:', A_T)

# Computing the inverse of the matrix
A_inv = np.linalg.inv(A)
print('Inverse of A:', A_inv)

# Computing the determinant of the matrix
A_det = np.linalg.det(A)
print('Determinant of A:', A_det)

# Computing the eigenvalues and eigenvectors of the matrix
eigenvalues, eigenvectors = np.linalg.eig(A)
print('Eigenvalues of A:', eigenvalues)
print('Eigenvectors of A:', eigenvectors)

The conjugate transpose, also known as Hermitian transpose, of a matrix can be computed using the .conj().T method.

import numpy as np
# Creating a 2D array (matrix) with complex numbers
A = np.array([[1+2j, 3+4j], [5+6j, 7+8j]], dtype='complex')

# Computing the conjugate transpose of the matrix
A_H = A.conj().T
print('Conjugate Transpose of A:', A_H)

Another example using complex numbers:

import numpy as np
# Creating a 2D array (matrix) with complex numbers
A = np.array([[1+2j, 3+4j], [5+6j, 7+8j]], dtype='complex')

# Computing the conjugate transpose of the matrix
A_H = A.conj().T
print('Original Matrix A:\n', A)
print('Conjugate Transpose of A:\n', A_H)

# Verifying that the conjugate transpose is correct
print('A * A_H:\n', A @ A_H)

Short exercise: Create a 2x2 matrix and compute its transpose, inverse, determinant, and eigenvalues/eigenvectors.

1.5 Some useful Numpy tools#

1.5.1 Random Number Generation#

Numpy provides a module called numpy.random that contains functions for generating random numbers and performing random sampling. Some commonly used functions include:

Function

Description

Example

np.random.rand(d0, d1, ..., dn)

Generates an array of the given shape and populates it with random samples from a uniform distribution over [0, 1).

random_array = np.random.rand(2, 3)

np.random.randn(d0, d1, ..., dn)

Generates an array of the given shape and populates it with random samples from a standard normal distribution (mean 0, variance 1).

random_array = np.random.randn(2, 3)

np.random.randint(low, high=None, size=None, dtype=int)

Generates random integers from low (inclusive) to high (exclusive).

random_integers = np.random.randint(0, 10, size=(2, 3))

np.random.choice(a, size=None, replace=True, p=None)

Generates a random sample from a given 1D array.

random_sample = np.random.choice([1, 2, 3, 4, 5], size=3)

np.random.normal(loc=0.0, scale=1.0, size=None)

Generates random samples from a normal (Gaussian) distribution with specified mean (loc) and standard deviation (scale).

random_samples = np.random.normal(loc=0, scale=1, size=100)

1.6 Polynomial Fitting with Numpy#

Numpy provides a convenient function np.polyfit() to fit a polynomial of a specified degree to a set of data points. The function takes in the x and y coordinates of the data points, the degree of the polynomial, and returns the coefficients of the fitted polynomial.

\[p(x,n) = c[0]x^n + c[2]x^{n-1} +...+ c[n-1]x ^1 + c[n]x^0 \]

where \(c[i]\) is the i-th coefficient.

In Numpy we can perform many operations with polynomials.

Some useful polynomial functions in Numpy:

Function

Description

Example

np.polyfit(x, y, deg)

Fits a polynomial of degree deg to the data points (x, y).

coefficients = np.polyfit(x, y, 2)

np.polyval(p, x)

Evaluates a polynomial at specific values of x.

y_fit = np.polyval(coefficients, x)

np.polyder(p, m=1)

Computes the derivative of a polynomial.

p_derivative = np.polyder(coefficients)

np.polyint(p, m=1, k=0)

Computes the integral of a polynomial.

p_integral = np.polyint(coefficients)

np.roots(p)

Computes the roots of a polynomial.

roots = np.roots(coefficients)

import numpy as np
# Generating some sample data
x = np.linspace(-5, 5, 100)
y = 2*x**3 - 4*x**2 + 3*x + np.random.normal(0, 10, size=x.shape)

# Fitting a polynomial of degree 3 to the data
degree = 3
coefficients = np.polyfit(x, y, degree)

# Evaluating the fitted polynomial at the x values
y_fit = np.polyval(coefficients, x)

# Printing the coefficients of the fitted polynomial
print("Coefficients of the fitted polynomial:", coefficients)

Short exercise: Generate some sample data points that follow a quadratic relationship with some added noise. Fit a polynomial of degree 2 to the data and plot both the original data points and the fitted polynomial curve.