Python for Quantum Mechanics: Numpy Basics#

from IPython.display import YouTubeVideo
YouTubeVideo('508EUmhzjPs',width=700, height=400)

Numpy Arrays#

import numpy as np

Numpy uses ojects known as arrays; the creation of one can be seen below. To call functions that are in numpy one uses ‘np.’ before the function.

arr = np.(object, dtype)

Lets use a list as our “object” and also set the data type to “float”.

arr = np.array([1,2.0], dtype=float)

print(arr)

print(type(arr))
print(arr.dtype)
[1. 2.]
<class 'numpy.ndarray'>
float64

We can similary use a tuple, or any array-like object, in the definition.

arr = np.array((1.0,2), dtype=int)

print(arr)

print(type(arr))
print(arr.dtype)
[1 2]
<class 'numpy.ndarray'>
int64

We access the data in the following way

print(arr[0])
1

Notice also that the data types of our list and tuple were recast according to the data type given in the definition.

These numpy array objects are like lists but have many more structures and functions that come along with them that make them easy to use. They are also significantly faster to use in computations than Python lists. A resulting constraint of this speed-up is that numpy arrays must contain elements of the same data type, as opposed to python lists which can contain multiple different data types.

What happens when we don’t specify the data type?

arr = np.array([1.0,1])
print(arr)
[1. 1.]
arr = np.array([1,1.0])
print(arr)
[1. 1.]

Notice that the default data type is set to the minimum data type required to store all elements in the array-like object. Floats require more memory than integers, so in the above cases the data type is set to float.

There are also some numpy array methods we can call on to describe our arrays:

print(arr.shape) #(Rows,Columns)
print(arr.size)  #Rows x Columns
print(arr.ndim)  #Dimensions of array
(2,)
2
1

Copying Arrays#

If we try to copy the array by assignment, this will reference to the original array, so be careful

arr = np.array([1,2,3,4,5])
cop = arr

cop[2] = 10

print(arr)
print(cop)
[ 1  2 10  4  5]
[ 1  2 10  4  5]

arr.copy()

We use the \(copy()\) method to actually copy an array.

arr = np.array([1,2,3,4,5])
cop = arr.copy()

cop[2] = 10

print(arr)
print(cop)
[1 2 3 4 5]
[ 1  2 10  4  5]

Basic Operators#

We can use the basic mathematical operators ( +, -, *, /, ** ) on Numpy arrays. They are applied element-wise.

arr1 = np.array([1,2,3,4,5])
arr2 = np.array([6,7,8,9,10])

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)
arr1 + arr2 =  [ 7  9 11 13 15]
arr1 - arr2 =  [-5 -5 -5 -5 -5]
arr1 * arr2 =  [ 6 14 24 36 50]
arr1 / arr2 =  [0.16666667 0.28571429 0.375      0.44444444 0.5       ]
arr1 ** arr2 =  [      1     128    6561  262144 9765625]

Numpy Constants#

print('pi = ',np.pi)
print('e = ',np.e)
print('euler gamma = ',np.euler_gamma)

print('infinity = ',np.Inf)
print('infinity = ',np.Infinity)
print('infinity = ',np.inf)
print('infinity = ',np.infty)
print('positive infinity = ',np.PINF)
print('negative infinity = ',np.NINF)

print('positive zero = ',np.PZERO)
print('negative zero = ',np.NZERO)

print('nan = ',np.nan)
print('nan = ',np.NAN)
print('nan = ',np.NaN)

print('None/newaxis = ',np.newaxis)
pi =  3.141592653589793
e =  2.718281828459045
euler gamma =  0.5772156649015329
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[12], line 5
      2 print('e = ',np.e)
      3 print('euler gamma = ',np.euler_gamma)
----> 5 print('infinity = ',np.Inf)
      6 print('infinity = ',np.Infinity)
      7 print('infinity = ',np.inf)

File /opt/hostedtoolcache/Python/3.12.6/x64/lib/python3.12/site-packages/numpy/__init__.py:400, in __getattr__(attr)
    397     raise AttributeError(__former_attrs__[attr], name=None)
    399 if attr in __expired_attributes__:
--> 400     raise AttributeError(
    401         f"`np.{attr}` was removed in the NumPy 2.0 release. "
    402         f"{__expired_attributes__[attr]}",
    403         name=None
    404     )
    406 if attr == "chararray":
    407     warnings.warn(
    408         "`np.chararray` is deprecated and will be removed from "
    409         "the main namespace in the future. Use an array with a string "
    410         "or bytes dtype instead.", DeprecationWarning, stacklevel=2)

AttributeError: `np.Inf` was removed in the NumPy 2.0 release. Use `np.inf` instead.

Mathematical Functions#

Numpy has built-in mathemtical functions. These include trigonmetric, hyperbolic, rounding, sums, products, differences, exponents, logarithms, and many more. An entire list can be found at https://numpy.org/doc/stable/reference/routines.math.html

They are generally applied element-wise, here are a few examples:

  • sin

  • cos

  • exp

  • sqrt

  • log

arr = np.array([1,2,3,4,5])

print('sin(arr) = ', np.sin(arr))

print('cos(arr) = ', np.cos(arr))

print('exp(arr) = ', np.exp(arr)) # e**x

print('sqrt(arr) = ', np.sqrt(arr))

print('log(arr) = ', np.log(arr)) # The natural log
sin(arr) =  [ 0.84147098  0.90929743  0.14112001 -0.7568025  -0.95892427]
cos(arr) =  [ 0.54030231 -0.41614684 -0.9899925  -0.65364362  0.28366219]
exp(arr) =  [  2.71828183   7.3890561   20.08553692  54.59815003 148.4131591 ]
sqrt(arr) =  [1.         1.41421356 1.73205081 2.         2.23606798]
log(arr) =  [0.         0.69314718 1.09861229 1.38629436 1.60943791]

0-dimensional Arrays#

We have been working with one-dimensional arrays so far, it turns out we can make 0-dimensional arrays. They are not very useful though

arr0 = np.array(2)
print(arr0)
print(arr0.ndim)
2
0

Multi-Dimensional Numpy Arrays#

Two-Dimensional Numpy Arrays#

We can also input multi-dimensional objects.This is very useful for handling matrices and vectors,or linear algebra more generally, which are key in the world of quantum computing. Recall our usage of nested lists.

H = [[1,1],[1,-1]]
X = [[0,1],[1,0]]

Hmat = np.array(H,float)/(np.sqrt(2))
Xmat = np.array(X,float)

print('H = {}'.format(Hmat))
print('X = {}'.format(Xmat))
H = [[ 0.70710678  0.70710678]
 [ 0.70710678 -0.70710678]]
X = [[0. 1.]
 [1. 0.]]

We access the data in a different way to lists

print(H[1][1])#This accesses data in a 2-d list

print(Hmat[1,1])#This accesses data in a 2-d numpy arrray
-1
-0.7071067811865475
print('shape = (Rows,Columns) = {}'.format(Hmat.shape))
print('size = Rows x Columns = {}'.format(Hmat.size))
print('ndim = number of dimensions = {}'.format(Hmat.ndim))
shape = (Rows,Columns) = (2, 2)
size = Rows x Columns = 4
ndim = number of dimensions = 2

Multiplying Matrices & Vectors#

np.matmul(A,B)

Lets use the \(np.matmul()\) method to multiply two matrices. First note that order of multiplication with matrices matters!

A = np.matmul(Xmat,Hmat)
B = np.matmul(Hmat,Xmat)

print(A,'\n') #'\n' recall this just moves to a new line

print(B,'\n')

print(A==B)
[[ 0.70710678 -0.70710678]
 [ 0.70710678  0.70710678]] 

[[ 0.70710678  0.70710678]
 [-0.70710678  0.70710678]] 

[[ True False]
 [False  True]]

Now lets multiply three matrices together, $\(HXH\)\( We can nest the \)np.matmul()$ to accomplish this faster

Zmat = np.matmul( Hmat , np.matmul(Xmat,Hmat) )
print('Z = {}'.format(Zmat))
Z = [[ 1.00000000e+00  2.23711432e-17]
 [-2.23711432e-17 -1.00000000e+00]]

We get an ugly answer because we are using floats of an irrational number \(\frac{1}{\sqrt{2}}\), but notice the negligibility of numbers of order \(10^{-17}\). Lets tidy this up with the \(np.round()\) method

np.round(array, decimals=0)

Zmat = np.round(Zmat) 
print('Z = {}'.format(Zmat))
Z = [[ 1.  0.]
 [-0. -1.]]

We still get an ugly \(-0.0\), but whatever…

Now lets make a 2-dimensional vector

obj = [[1],[0]]
Vec = np.array(obj, dtype = float)
print(Vec)
[[1.]
 [0.]]

Lets rotate it by multiplying it by the X matrix.(Recall week 2 exercises).

VecRot = np.matmul(Xmat,Vec)
print(VecRot)
[[0.]
 [1.]]

Now let’s dip into the \(np.linalg\) methods, namely \(np.linalg.norm()\) which calculates the modulus(or norm) of a vector

np.linalg.norm(array)

print('Vector modulus = {}'.format(np.linalg.norm(Vec)))
print('Rotated vector modulus = {}'.format(np.linalg.norm(VecRot)))
Vector modulus = 1.0
Rotated vector modulus = 1.0

We can see that numpy provides a vast array of methods and functionality to work with the mathematical ideas associated with linear algebra(matrices, vectors, their operations and properties)

Three-Dimesional Numpy Arrays#

A less common use, but still an interesting study, is for 3-dimensional arrays.

obj = [  [ [1,0,0] , [0,-1,0] , [0,0,1] ] , [ [1,3,2] , [3,2,1] , [2,1,3] ] ]
mat = np.array(obj,int)
print(mat)
[[[ 1  0  0]
  [ 0 -1  0]
  [ 0  0  1]]

 [[ 1  3  2]
  [ 3  2  1]
  [ 2  1  3]]]
print(mat.shape) # 2 elements in the first nest, 3 elements in the second nest, 3 elements in the third nest
(2, 3, 3)
print(mat.size) # Multiply the elements of mat.shape
18
print(mat.ndim) #Number of nests
3