Normalization and unitary matrices

Normalization

When we start working with quantum states in the form of vectors, there are a couple of things that we’ll need to keep in mind. First is the notion of normalization. Normalization is a word which appears in any number of fields and typically means to scale you object so that its size (however you define that) is equal to one. In our case, vectors have a notion of length, more properly called the norm, and it is this that we scale to one. A vector is only a valid quantum state if it has norm one, and a matrix is only a valid quantum operator if it preserves norms.

Calculating the norm of a vector is done using nothing but the good old Pythagorean formula. Just as the length of a vector in Euclidean space is given by the root of the sum of the squares along the axes, the norm of a quantum state is given by $$\textrm{norm}(\psi) = \sqrt{|\psi_0|^2+|\psi_1|^2+\dots+|\psi_n|^2}.$$ We’ve taken the absolute values of the elements because $\psi_i$ is generally complex. For real numbers the absolute value emerges automatically. To normalize a vector we simply divide each element by the norm of the whole vector. In code, we first calculate this sum of squares and then use the scalar_matrix() function from earlier to divide every element by it.

def normalize(vector):
    from math import sqrt
    elements = flatten(vector)[0]
    norm = sqrt(sum([abs(element)**2 for element in elements]))
    return scalar_matrix(1/norm, vector)

abs() is another built-in Python function which gives the absolute value of a number. We imported sqrt() from the math module, which should normally be done at the top of a file, but I did it here so we can’t forget. I pre-flattened the vector so that it wouldn’t be necessary to check whether it’s a column or row. Finally I return the original vector divided by the norm using scalar_matrix().

Before moving on it might be worth knowing that for most operations we can skip normalization steps, and just normalize at the end. However this isn’t always the case, so when we come to defining our quantum operators it will be worth making sure they can only ever return a normalized vector, using the function we’ve just written. It will save us some headaches in the long run.

Unitary matrices

Matrices also have a notion of normality, and an additional property called unitarity. What we really care about is unitarity, because these are the norm-preserving operations in a vector space. In general, a unitary matrix can be regarded as a rotation in a vector space, and these are the types of operations we will want in a quantum computer. Matrices are normal and unitary if, respectively, $$\mathbb{N}^\dagger\mathbb{N} = \mathbb{N}\mathbb{N}^\dagger,\qquad \mathbb U^\dagger \mathbb U = \mathbb U\mathbb U^\dagger = I,$$ where \(I\) is the identity matrix of appropriate size. It should be easy enough to convince yourself that a unitary matrix is definitely normal, but a normal matrix is not necessarily unitary.

The symbol you’re probably wondering about is generally read as ‘dagger’, and refers to the complex conjugate transpose of that matrix. We transpose the matrix, and take the complex conjugates of all its elements (either way round is fine). Transposing a matrix is very simply flipping all of the elements over the main diagonal, so that element \((i,j)\) becomes element \((j,i)\) for all \(i,j\). You won’t be surprised to learn that this is a relatively simple piece of code:

def transpose(matrix):
    row_count = range(rows(matrix))
    col_count = range(columns(matrix))
    return [[matrix[j][i] for j in row_count] for i in col_count]

For the complex conjugates, we can use the handy .conjugate() method of the complex number class. Unfortunately this isn’t defined for ints or floats (I suppose there’s no reason it should be, even though it is mathematically valid if pointless to take the conjugate of a real number). This means that we’ll need to check each element actually is complex before attempting to conjugate it, and ignore that element if it isn’t complex.

def conjugate_matrix(matrix):
    return [[element.conjugate() if isinstance(element, complex) else element for element in row] for row in matrix]

We have used another Python built-in, isinstance(), to check that an element is actually complex. Again we see a ternary operation; the element is conjugated if it is complex, and is left alone otherwise. These are both useful operations in a wealth of circumstances, but for now what we really want is a way to compute the conjugate transpose, also called the adjoint, of a matrix, which is literally just both of these operations in either order. So,

def adjoint(matrix):
    return conjugate_matrix(transpose(matrix))

Returning to the main point, we want to check for unitarity. Simply

def is_unitary(matrix):
    size = rows(matrix)
    Udagger = adjoint(matrix)
    U = matrix
    return matrix_matrix(Udagger, U) == eye(size) and matrix_matrix(U, Udagger) == eye(size)

U = matrix was stated just to make the final line more readable. I haven’t mentioned it yet but unitaries are generally square so rows(matrix) returns the size we need to feed to eye(). You see an and statement in the return line; the function returns True if and only if both of the statements there are True, and False otherwise. True and False, case sensitive, are the labels for those boolean values in Python.

Summary

So far our functional repertoire is:

  • rows(matrix): count the number of rows in a matrix.
  • columns(matrix): count the number of columns in a matrix.
  • add(matrix1, matrix2): compute the element-wise sum of two matrices.
  • subtract(matrix1, matrix2): compute the element-wise difference of two matrices.
  • zeroes(num_rows, num_cols=-1): generate a matrix of the given size filled with zeroes.
  • ones(num_rows, num_cols=-1): generate a matrix of the given size filled with ones.
  • eye(size): generate a square identity matrix of the given size.
  • scalar_matrix(scalar, matrix): compute the product of a scalar value and a matrix.
  • get_row(matrix, row_num): grab a particular matrix row and return it as a valid matrix.
  • get_column(matrix, col_num): grab a particular matrix column and return it as a valid matrix.
  • normalize(vector): returns a vector scaled to a norm of 1.
  • transpose(matrix): returns the transpose of a matrix or vector.
  • conjugate_matrix(matrix): computes the element-wise conjugate of a matrix.
  • adjoint(matrix): computes the conjugate transpose of a matrix.
  • is_unitary(matrix): checks a matrix for unitarity.

Previous: Matrix multiplication and the inner product
Next: The Kronecker product