A Quantum Computing Library in 48 Lines of Python
Quantum Computing has a reputation for being hard. This doesn’t have to be the case. We are going to implement a Quantum Computing library using only 48 lines of Python, and explain every line.
Postulates of Quantum Mechanics
Quantum Mechanics is the arena in which all of physics takes place. Physics can get complicated, but the arena is simple. There are only 4 rules in all of Quantum Mechanics. They describe:
- States
- Operations
- Composition
- Measurement
States and operations, variables and functions, systems and transformations, operands and operators, the way things are and the way things change. This is the language of Quantum Mechanics.
First, some terminology. When physicists say “system”, they can mean anything. An electron, a pair of dice, a single bit in a computer, a single qubit in a quantum computer, a cat inside a box, or the entire universe. These are all examples of physical systems.
A system is called “closed” if it doesn’t interact with the outside world. The rules are always more simple if there’s no outside interference, so we’re only going to be talking about closed systems. If a system is not closed, you can always just consider the larger system composed of the non-closed system + external interferer.
States
Rule 1: Any system that can be in certain states can also be in any unit length complex combination of those states.
The most simple possible system is one that can only take on two values. Let’s call our two values “up” and “down”. Rule 1 says actually (1/√2) up + (1/√2) down is also a valid state of this system. The amount that a state is up or down can also be negative or even imaginary, another valid state is i(1/√2) up - (1/√2) down. Let’s use vectors to represent states.
We can use numpy.linalg.norm
to get the length of the complex vector and see if it’s 1. We’ll use an error tolerance because we’ll lose some precision along the way if we’re doing things like square roots.
class QuantumState:
def __init__(self, vector):
length = numpy.linalg.norm(vector)
if not abs(1 - length) < 0.00001:
raise ValueError('Quantum states must be unit length.')
self.vector = numpy.array(vector)
Operations
Rule 2: All physically possible state transformations are unitary on the state space.
A unitary transformation is one that doesn’t change the length of any vector, or the relative distances between vectors. It’s like a generalized rotation. This is the only way things can change states in a closed system in this universe. All physical forces, all computations, all actions that a person can take, are unitary.
A unitary transformation on a vector can be represented by a unitary matrix, and the output of the operation is given by a matrix multiplication between the operation matrix and the state vector.
This is enough to implement operation initialization and application.
We’ll implement an is_unitary
helper function using the fact that a matrix M is unitary if and only if M*M=I, where M* is the conjugate transpose of M, and I is the identity matrix. We’ll use numpy.allclose
in order to have an error tolerance once again.
def is_unitary(M):
M_star = numpy.transpose(M).conjugate()
identity = numpy.eye(len(M))
return numpy.allclose(identity, numpy.matmul(M_star, M))class QuantumOperation:
def __init__(self, matrix):
if not is_unitary(matrix):
raise ValueError('Quantum operations must be unitary')
self.matrix = matrix def apply(self, state):
new_vector = numpy.matmul(self.matrix, state.vector)
return QuantumState(new_vector)
Composition
Rule 3: The combined state of two systems or transformations is their Kronecker product.
If we have two independent systems and we want to talk about the system that is the combination of the two systems, we do so by combining them using a Kronecker product. This applies to both state vectors and matrix operators.
In the picture above, the right side is decomposable into the left side. However, not all combined states have this property. The phenomenon called entanglement occurs when you take the composition of 2 systems, then you scramble it up by applying a unitary operator to the combined system that makes it impossible to decompose cleanly back into 2 independent systems. In Quantum Mechanics, the whole can be more than the sum of its parts.
We can use numpy.kron
to implement composition.
class QuantumState:
def compose(self, state):
new_vector = numpy.kron(self.vector, state.vector)
return QuantumState(new_vector)class QuantumOperation:
def compose(self, operation):
new_matrix = numpy.kron(self.matrix, operation.matrix)
return QuantumOperation(new_matrix)
Measurement
Rule 4: When a state is measured, it takes on one of its classical states at random, weighted by the squares of the absolute values of the complex numbers corresponding to that state.
This explains why Rule 1 requires that states have length 1: Because they act as a probability distribution over measurement outcomes.
class QuantumState:
def measure(self):
choices = range(len(self.vector))
weights = [abs(a)**2 for a in self.vector]
outcome = random.choices(choices, weights)[0] new_state = numpy.zeros(len(self.vector))
new_state[outcome] = 1
self.vector = new_state
return outcome
Aside: Many Worlds
Notice that measurements are not unitary. The state immediately and discontinuously jumps to the measured outcome upon measurement. What even counts as a measurement? Is it when a human experimenter perceives the outcome? Aren’t humans just large quantum systems who should be evolving unitarily along the same laws of physics as everything else?
The solution accepted by many physicists is this: measurements aren’t really some 4th rule that is different from regular unitary evolution, all that’s happening is that the observer is becoming entangled with the system.
You only think you’re observing up with |α|² probability and down with |β|² probability, when in fact what’s happening is that α of you is now in the branch of the universe where the qubit is up and β of you is in the other. Both branches exist but you only perceive one because your consciousness splits.
Aside: More General Models
I’ve presented the rules in this article in their most simple form. Other sources will usually present them in a more general way.
If you want to talk about the state of a subset of an entangled system (combining Rules 1 and 3), then you should use density matrices instead of unit length complex vectors to represent states.
If you want to do something that is partially an operation, partially a measurement, and you want it to apply to a subset of an entangled system (combining Rules 2, 3 and 4), then instead of unitary matrices you should use completely-positive trace-preserving maps.