Quantum Mechanics 101: Demystifying Its Postulates¶
If quantum mechanics hasn't profoundly shocked you, you haven't understood it yet.
-- Niels Bohr
Quantum mechanics is often described as one of the most mind-bending fields of physics, not just because of its abstract nature but because of the very real, fundamental concepts it reveals about our universe. As someone who first encountered quantum mechanics in my sophomore physics course, I can attest to the confusion and awe that come with it. From the strange notion of particles existing as a probability until observed, to the idea of wavefunctions that govern everything, the journey through quantum mechanics is both intellectually thrilling and deeply perplexing.
Over the years, whether through my undergraduate or master's courses, I repeatedly encountered concepts that initially seemed bizarre and not very application-oriented. Among these were ideas such as the unitarity of operators, time evolution, time-dependent versus time-independent states, and the Hermiticity of operators. At first, these principles felt abstract and difficult to apply outside the classroom. However, as I progressed into more advanced courses like Quantum Field Theory, these concepts became essential to understanding the subject. Often, it takes multiple reviews, months of rethinking, and even fresh perspectives to truly make the connections.
While quantum mechanics is an immensely powerful framework for understanding the microscopic world, it can be frustratingly abstract, requiring a solid understanding of both its mathematical structure and the physical intuition behind it. In this article, I aim to offer an accessible explanation of these foundational ideas for undergraduates or graduate students, with a focus on understanding quantum mechanics from a more tangible, relatable viewpoint.
Classical vs Quantum: The Idea of the Qubit¶
In classical mechanics, the most basic unit of information is the bit, which represents a binary value—either 0 or 1. Imagine these bits as simple "on" or "off" states, much like flipping a light switch.
Quantum mechanics, however, introduces a new and strange concept: the quantum bit or qubit. Unlike a classical bit, a qubit can exist in a state that is both 0 and 1 simultaneously, a phenomenon known as superposition.
A qubit is described mathematically as:
$$ |\psi\rangle = \alpha |0\rangle + \beta |1\rangle $$
where $ \alpha $ and $ \beta $ are complex numbers that satisfy the normalization condition:
$$ |\alpha|^2 + |\beta|^2 = 1 $$
Here, $ |0\rangle $ and $ |1\rangle $ represent the classical bit states, but now, the qubit can exist in a linear combination of these states. The coefficients $ \alpha $ and $ \beta $ determine the probability amplitudes for the system being observed in either of these two states.
But what exactly is a qubit physically? To dive deeper into this, we turn to the postulates of quantum mechanics, which govern the behavior of quantum systems.
Postulates of Quantum Mechanics¶
The postulates of quantum mechanics provide the foundational principles that govern the behavior of quantum systems. These postulates are the rules that form the structure of quantum mechanics, and they help explain phenomena such as wave-particle duality, superposition, and entanglement. Here is a summary of the key postulates:
Postulate 1: State Representation and Quantum Space¶
In quantum mechanics, the state of a quantum system is represented by a vector in a complex vector space (specifically, a Hilbert space). We represent the state of a system as a ket $ |\psi\rangle $, a unit vector in this space. In the language of bra-ket notation developed by Paul M. Dirac, the corresponding bra $ \langle \psi| $ is the dual of the ket.
In a more familiar notion that's in matrix form, the state $ |\psi\rangle $ is represented as a column vector, while the corresponding dual $ \langle \psi| $ is the complex conjugate transpose (row vector) of that vector. For example, if we have a quantum system in a $d-$dimensional space, the state vector can be written as:
$$ |\psi\rangle = \begin{pmatrix} \alpha_1 \\ \alpha_2 \\ \vdots \\ \alpha_d \end{pmatrix} $$
The corresponding dual (bra) would be the complex conjugate transpose of the ket:
$$ \langle \psi| = (\alpha_1^*, \alpha_2^*, \dots, \alpha_d^*) $$
The inner product between two vectors $ |\psi\rangle $ and $ |\phi\rangle $ is defined as:
$$ \langle \psi | \phi \rangle = \sum_i \alpha_i^* \beta_i $$
This gives a scalar quantity.
And the outer product (ket-bra) results in a matrix:
$$ |\psi\rangle \langle \phi| $$
This is the basis for constructing linear operators on the state space. The outer product (ket-bra) produces a matrix, and these matrices can be interpreted as linear operators acting on vectors within a vector space.
The Normalization Condition¶
Quantum states are normalized, meaning the total probability of finding the system in any of its possible states must be 1. This normalization condition is written as:
$$ \langle \psi | \psi \rangle = 1 $$
This condition is referred to as the normalization condition. It is not a purely mathematical constraint, but rather arises from a physical consideration, namely, the conservation of probabilities. The components of this vector are probability amplitudes, and thus the square of these amplitudes must sum to 1.
If $ |\psi\rangle \in \mathcal{H} $, then any scalar multiple $ \lambda |\psi\rangle $ is also an element of $ \mathcal{H} $, where $ \lambda $ is a scalar. Physically, $ \psi $ and $ \lambda |\psi\rangle $ represent the same quantum state, as $ \lambda $ does not produce any observable distinction. Therefore, quantum states are defined up to an overall scale, which is often referred to as the "overall phase." This freedom allows us to normalize the vector, which does not alter the state itself.
The Bloch Sphere¶
Because the state of a qubit is represented by two complex numbers (i.e., $ \alpha $ and $ \beta $), we can parameterize these states in terms of two real angles—$ \theta $ and $ \phi $:
$$ \alpha = \cos\left(\frac{\theta}{2}\right) e^{i \phi_{\alpha}}, \quad \beta = \sin\left(\frac{\theta}{2}\right) e^{i \phi_{\beta}} $$
where $\theta \in [0, \pi]$ and $\phi_{\alpha}, \phi_{\beta} \in [0, 2\pi]$, due to the overall phase freedom of the state.
Thus, the qubit state can be expressed as:
$$ |\Psi\rangle = \cos\left(\frac{\theta}{2}\right) e^{i \phi_{\alpha}} |0\rangle + \sin\left(\frac{\theta}{2}\right) e^{i \phi_{\beta}} |1\rangle $$
or equivalently,
$$ |\Psi\rangle = e^{i \phi_{\alpha}} \left[\cos\left(\frac{\theta}{2}\right) |0\rangle + \sin\left(\frac{\theta}{2}\right) e^{i (\phi_{\beta} - \phi_{\alpha})} |1\rangle \right] $$
This representation describes the state as a point on the surface of a sphere in 3D space, known as the Bloch sphere.
Basis States and Transformations¶
A vector space can have infinitely many basis states. In addition to the standard computational basis $\{|0\rangle, |1\rangle\}$, the qubit space can also have other valid basis sets. Two alternative orthonormal basis states for $\mathbb{C}^2$ are:
$$ |+\rangle = \frac{1}{\sqrt{2}} (|0\rangle + |1\rangle), \quad |-\rangle = \frac{1}{\sqrt{2}} (|0\rangle - |1\rangle) $$
These states form a new orthonormal basis, and we can verify the orthogonality: $$ \langle + | - \rangle = \langle - | + \rangle = 0, \quad \langle + | + \rangle = \langle - | - \rangle = 1 $$
Thus, $\{|+\rangle, |-\rangle\}$ also forms an orthonormal basis for the qubit state space.
We can also define another set of basis states:
$$ |i\rangle = \frac{1}{\sqrt{2}} (|0\rangle + i |1\rangle), \quad |-i\rangle = \frac{1}{\sqrt{2}} (|0\rangle - i |1\rangle) $$
The set $\{|i\rangle, |-i\rangle\}$ forms another orthonormal basis for the qubit state space $\mathbb{C}^2$.
These new basis states are related to the computational basis via a unitary transformation, which is a linear transformation that preserves the inner product of vectors.
These different basis sets are related through a linear transformation. For example, the transformation between the computational basis $\{|0\rangle, |1\rangle\}$ and the $\{|+\rangle, |-\rangle\}$ basis can be represented by a linear operator, such as the Hadamard operator $H$:
$$ H |0\rangle = |+\rangle = \frac{1}{\sqrt{2}} (|0\rangle + |1\rangle), \quad H |1\rangle = |-\rangle = \frac{1}{\sqrt{2}} (|0\rangle - |1\rangle) $$
This transformation can be written in terms of outer products:
$$ \mathcal{H} = \frac{1}{\sqrt{2}} \left( |+\rangle \langle 0| + |-\rangle \langle 1| \right) $$
Alternatively, it can be expressed as:
$$ \mathcal{H} = \frac{1}{\sqrt{2}} \left( |0\rangle \langle 0| + |1\rangle \langle 0| + |0\rangle \langle 1| - |1\rangle \langle 1| \right) $$
Matrix Representation¶
The state vectors $|0\rangle$ and $|1\rangle$ are represented as column vectors:
$$ |0\rangle = \begin{pmatrix} 1 \\ 0 \end{pmatrix}, \quad |1\rangle = \begin{pmatrix} 0 \\ 1 \end{pmatrix} $$
The alternative basis states $|+\rangle$ and $|-\rangle$ are represented as:
$$ |+\rangle = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 \\ 1 \end{pmatrix}, \quad |-\rangle = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 \\ -1 \end{pmatrix} $$
Using these, we can express the Hadamard operator $H$ as:
$$ H = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 & 1 \\ 1 & -1 \end{pmatrix} $$
The Hadamard gate is an important quantum operation that transforms the computational basis states into the $\{|+\rangle, |-\rangle\}$ basis.
State $|0\rangle$:¶
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
from numpy import pi
qreg_q = QuantumRegister(1, 'q')
circuit = QuantumCircuit(qreg_q)
psi = Statevector(circuit)
plot_bloch_multivector(psi)
circuit.draw(output = "mpl")
DensityMatrix(psi).draw("latex") # convert to a DensityMatrix and draw
psi = Statevector(circuit)
plot_bloch_multivector(psi)
plot_state_city(psi)
plot_state_qsphere(psi)
State $|1\rangle$:¶
qreg_q = QuantumRegister(1, 'q')
creg_c = ClassicalRegister(1, 'c')
circuit = QuantumCircuit(qreg_q, creg_c)
circuit.x(qreg_q[0])
#circuit.measure(qreg_q[0], creg_c[0])
<qiskit.circuit.instructionset.InstructionSet at 0x7b24fcb86380>
circuit.draw(output = "mpl")
psi = Statevector(circuit)
from qiskit.visualization import plot_bloch_multivector
plot_bloch_multivector(psi)
from qiskit.quantum_info import DensityMatrix
DensityMatrix(psi).draw("latex") # convert to a DensityMatrix and draw
from qiskit.visualization import plot_state_city
plot_state_city(psi)
from qiskit.visualization import plot_state_qsphere
plot_state_qsphere(psi)
Superposition State¶
$|\psi\rangle = \frac{1}{\sqrt{2}}\left(|0\rangle+1\rangle\right)$
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
from qiskit.visualization import plot_bloch_multivector, plot_state_city, plot_state_qsphere
from qiskit.quantum_info import Statevector, DensityMatrix
import matplotlib.pyplot as plt
# --- Superposition State ---
# Create a quantum circuit for the superposition state
qreg_q = QuantumRegister(1, 'q')
creg_c = ClassicalRegister(1, 'c')
circuit_superposition = QuantumCircuit(qreg_q, creg_c)
# Apply Hadamard gate to create superposition state |+> = 1/sqrt(2) (|0> + |1>)
circuit_superposition.h(qreg_q[0])
# Calculate the statevector
psi_superposition = Statevector(circuit_superposition)
circuit_superposition.draw(output="mpl")
plot_bloch_multivector(psi_superposition)
plot_state_city(psi_superposition)
plot_state_qsphere(psi_superposition)
# Plot Probability Amplitudes (magnitude squared) for superposition state
probabilities = np.abs(psi_superposition.data)**2
plt.subplot(2, 3, 4)
plt.bar(range(len(probabilities)), probabilities, color='b', alpha=0.7)
plt.title('Probability Amplitudes (Superposition)')
plt.xlabel('State |0>, |1>')
plt.ylabel('Probability')
Text(0, 0.5, 'Probability')
plt.subplot(2,3,5)
plt.bar(range(len(psi_superposition.data)), np.real(psi_superposition.data), color='r', alpha=0.7, label='Real part')
plt.bar(range(len(psi_superposition.data)), np.imag(psi_superposition.data), color='b', alpha=0.7, label='Imaginary part')
plt.title('State Vector (Superposition)')
plt.xlabel('State |0>, |1>')
plt.ylabel('Amplitude')
plt.legend()
<matplotlib.legend.Legend at 0x7b24f8dc0850>
Two-Level Quantum Systems: The Qubit¶
The simplest example of a quantum system is a two-level system, which is the quantum analog of a classical bit. The state space of such a system is described by two orthogonal states, $ |0\rangle $ and $ |1\rangle $. These states are often called the computational basis.
Physical examples of two-level systems include:
The polarization of light, with horizontal $ |H\rangle $ and vertical $ |V\rangle $ polarization states. A general polarization state is:
$$ |P\rangle = \alpha |H\rangle + \beta |V\rangle \in \mathbb{C}^2 $$
We can also identify these states as:
$$ |H\rangle = |0\rangle, \quad |V\rangle = |1\rangle $$
The polarization states of light can be described as quantum superpositions, where the light can exist in a state that is simultaneously horizontal and vertical polarization. This concept is fundamental to quantum optics and quantum information theory.
The spin of particles: The spin of a particle (such as an electron or proton) can be probed by an external magnetic field. In the presence of a magnetic field, the particle’s spin states are described as spin-up and spin-down states. These correspond to the two-level system with states $ |0\rangle $ and $ |1\rangle $:
$$ |\Psi\rangle = \alpha |\uparrow\rangle + \beta |\downarrow\rangle \in \mathbb{C}^2 $$
The spin of these particles is a crucial quantum property, and when measured along any axis (typically the $ z $-axis), the spin is quantized into two distinct eigenstates—up and down—resulting in a binary state space. This concept forms the basis for many quantum technologies, including quantum computing and quantum cryptography.
Two-level atoms (Superconducting Qubits): In a system with a two-level atom, there are two distinct energy levels: the ground state $ |G\rangle $ and the excited state $ |E\rangle $. The state is described as:
$$ |\Psi\rangle = \alpha |G\rangle + \beta |E\rangle \in \mathbb{C}^2 $$
In atoms, quantum mechanics allows for the existence of discrete energy levels, and a two-level atom can exist in a superposition of these energy states. This phenomenon is crucial in many areas, such as quantum optics, laser physics, and the study of atomic transitions. The ability of an atom to exist in a superposition of ground and excited states is also the foundation of quantum information processing.
The state of a qubit can always be written as a linear combination of these two basis states:
$$ |\psi\rangle = \alpha |0\rangle + \beta |1\rangle $$
Postulate 2. Observables and Operators¶
Quantum mechanics also provides a framework for understanding observables, which are quantities we can measure in a quantum system. In classical mechanics, observables are just real numbers (like position or momentum), but in quantum mechanics, observables are represented by Hermitian operators. As we mentioned, states in quantum systems are represented as vectors, and they transform according to linear transformations. There are two important classes of linear operators we are concerned with:
1. Hermitian Matrices/Operators¶
A matrix or operator $H$ is called Hermitian if it satisfies the following condition:
$$ H = (H^*)^T = H^{\dagger} $$
where $H^{\dagger}$ is the adjoint of the linear operator, which refers to the complex conjugate transpose of $H$. Hermitian operators are also often referred to as self-adjoint operators.
Properties of Hermitian Matrices:¶
- Real Eigenvalues: Hermitian matrices have real eigenvalues.
- Diagonalizability: Hermitian matrices are diagonalizable, meaning they can be expressed in diagonal form in some orthonormal basis.
Example: Hermitian Operator for a Two-Level System¶
Consider the spin operator $S_z$ for a spin$-\frac{1}{2}$ particle. Its matrix representation is:
$$ S_z = \frac{\hbar}{2} \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix} $$
Other spin operators such as $S_x$ and $S_y$ are also Hermitian.
Why are Hermitian operators important in quantum mechanics?¶
Postulate 2 in quantum mechanics states that physical quantities or observables are associated with Hermitian operators on the system's Hilbert space. The eigenvalues of these Hermitian operators correspond to the possible values that the physical quantity can take.
For example, in the case of a spin-1/2 system:
The states $$|0\rangle = |1/2\rangle = \begin{pmatrix} 1 \\ 0 \end{pmatrix}$$ and $$|1\rangle = |-1/2\rangle = \begin{pmatrix} 0 \\ 1 \end{pmatrix}$$ are used to represent spin states.
For $S_z$, we have the eigenvalue equations:
$$ S_z|0\rangle = \frac{\hbar}{2} |0\rangle $$
$$ S_z |1\rangle = -\frac{\hbar}{2} |1\rangle $$
These are eigenvalue equations, implying that $S_z$ is the observable associated with the spin along the z-axis. In contrast, for $S_x$, we get:
$$ S_x |0\rangle = \frac{\hbar}{2} |1\rangle $$
$$ S_x |1\rangle = \frac{\hbar}{2} |0\rangle $$
These are not eigenvalue equations, so $S_x$ cannot be directly associated with a single value in this basis. However, when we express $S_x$ in the $\{|+\rangle, |-\rangle\}$ basis (the eigenstates of $S_x$), we find:
$$ S_x |+\rangle = \frac{\hbar}{2} |+\rangle $$
$$S_x |-\rangle = \frac{\hbar}{2} |-\rangle $$
This makes $S_x$ an observable associated with the spin component along the x-axis.
The matrix representations of these operators in the $\{|0\rangle, |1\rangle\}$ basis are:
$$ S_z = \frac{\hbar}{2} \left( |0\rangle \langle 0| - |1\rangle \langle 1| \right) $$
and
$$ S_x = \frac{\hbar}{2} \left( |0\rangle \langle 1| + |1\rangle \langle 0| \right) $$
These can also be written in the $\{|+\rangle, |-\rangle\}$ basis:
$$ S_x = \frac{\hbar}{2} \left( |+\rangle \langle -| + |-\rangle \langle +| \right) $$
2. Unitary Operators¶
A matrix or operator $U$ is unitary if it satisfies the following condition:
$$ U^{\dagger} U = U U^{\dagger} = I $$
where $I$ is the identity matrix. An example of a unitary operator is the Hadamard operator $H$, which is given by:
$$ H = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 & 1 \\ 1 & -1 \end{pmatrix} $$
The Hadamard operator acts on the computational basis states as follows:
$$ H |0\rangle = |+\rangle, \quad H |1\rangle = |-\rangle $$
The Hadamard operator is both Hermitian and unitary, meaning:
$$ H^{\dagger} = H \quad \text{and} \quad H H^{\dagger} = I $$
Why are unitary operators important?¶
Unitary operators are crucial in quantum mechanics, particularly for describing time evolution of quantum systems which bring in the next postulates of quantum mechanics.
Postulate 3: Time Evolution (How do Quantum Systems Evolve with Time?)¶
One of the most fundamental questions in quantum mechanics is how quantum systems evolve over time. This evolution is governed by the Schrödinger equation, which describes how the state of a quantum system changes with time.
The evolution of quantum systems over time is governed by unitary transformations. If $|\psi(t_0)\rangle$ represents the state of the system at time $t_0$, the state at time $t$ is given by:
$$ |\psi(t)\rangle = U(t, t_0) |\psi(t_0)\rangle $$
where $U(t, t_0)$ is the unitary time evolution operator. This operator governs how quantum states evolve over time.
The time evolution is driven by a Hermitian operator called the Hamiltonian $H$, which is associated with the energy of the system. The Schrödinger equation describes the time evolution of the system:
$$ i \hbar \frac{\partial}{\partial t} |\psi(t)\rangle = H |\psi(t)\rangle $$
For time-independent Hamiltonians, the solution to the Schrödinger equation is:
$$ |\psi(t)\rangle = \color{blue}{\mathbf{e^{i H(t - t_0)/\hbar}}} |\psi(t_0)\rangle $$
The term $\color{blue}{\mathbf{e^{i H(t - t_0)/\hbar}}}$ is the unitary time evolution operator $\color{blue}{\mathbf{U(t, t_0)}}$.
Why must time evolution be unitary?¶
To preserve the norm of the quantum state (since probabilities must sum to 1), the time evolution operator must be unitary. That is, for the initial state $|\psi(t_0)\rangle$ with norm 1:
$$ \langle \psi(t_0) | \psi(t_0) \rangle = 1 $$
The final state $|\psi(t)\rangle$ will also have norm 1:
$$ \langle \psi(t) | \psi(t) \rangle = \langle \psi(t_0) | U^{\dagger} U | \psi(t_0) \rangle = \langle \psi(t_0) | \psi(t_0) \rangle = 1 $$
Thus, unitary transformations preserve the norm of the state vector, making them essential for quantum mechanics.
Unitary time evolution operators form the quantum gates used in quantum computation, where single qubit gates are represented by $2\times2$ unitary matrices such as the Hadamard gate $H$.
Properties of the Time Evolution Operator $U(t, t_0)$:¶
Composition: The time evolution operator satisfies:
$$ U(t_2, t_0) = U(t_2, t_1) U(t_1, t_0) \quad \text{for} \quad t_0 \leq t_1 \leq t_2 $$ This property allows us to compose quantum gates in sequences.
Inverses: The inverse of the time evolution operator is:
$$ U^{\dagger}(t, t_0) = U(t_0, t) $$ This implies that quantum gates are reversible, a fundamental characteristic of quantum mechanics.
Postulate 4: Measurement and Probability¶
So far, we have seen that a quantum system or state must be represented by a unitary matrix, and it evolves over time through unitary transformations. Physical quantities are associated with Hermitian operators. But how do we know what physical values correspond to these quantities, and what is the process of measurement?
The process of measurement in quantum mechanics is not as straightforward as in classical mechanics. In classical mechanics, measurements can give us a definite value, but quantum mechanics introduces an element of uncertainty. When measuring physical quantities, the set of eigenvalues of the corresponding Hermitian operator gives the possible values that can be obtained from the measurement. Naturally, there is a probabilistic element to the quantum measurement process.
Let $A$ be a Hermitian operator (observable) with eigenvalues $\{a_i\}$ and eigenstates $|a_i\rangle$, where $i = 1, \dots, d$. Then, the postulate says that measuring the observable $A$ on the state $|\psi\rangle$ gives the outcome $a_i$ with probability:
$$ P_i = |\langle \psi | a_i \rangle|^2. $$
If you know the state of the system, you can predict the possible outcomes and their respective probabilities. However, each time you measure the system, you cannot predict which specific outcome will occur. By repeating the experiment many times, you can extract statistical data about the measurement outcomes.
- Post-measurement state: After obtaining the outcome $a_i$, the state $|\psi\rangle$ collapses to the corresponding eigenstate $|a_i\rangle$. More precisely, the state projects onto the eigenstate $|a_i\rangle$ associated with the measured value.
The act of measurement on quantum state $|\psi\rangle$ by apparatus $A$
Quantum Measurment using Qiskit¶
1. Measuring Spin state or qubit state $|0\rangle$¶
We have seen earlier that the states $|0\rangle$ and $|1\rangle$ can be mapped to the spin basis states $|+\rangle$ and $|-\rangle$ where $$ S_z|\pm\rangle =\frac{\hbar}{2} |\pm\rangle $$
In qiskit, note that the labels $|\pm\rangle = \frac{1}{\sqrt{2}}\left(|0\rangle\pm |1\rangle\right)$ not the eigenstates of $S_z$.
Following the above single-spin code, let's create a single-qubit circuit:
Note: By default, each qubit is initialized to the state $|0\rangle$. That means, we can pretend our quantum circuit represents a collection of spin-1/2 degrees of freedom initialized in $|+\rangle$.
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
from numpy import pi
qreg_q = QuantumRegister(1, 'q')
creg_c = ClassicalRegister(1, 'c')
circuit = QuantumCircuit(qreg_q, creg_c)
circuit.measure(qreg_q,creg_c)
circuit.draw('mpl')
It's best to run quantum circuits on a simulator before using actual quantum hardware, as access to real machines is limited and can be delayed due to high demand. A simulator generates the ideal, noiseless output of a circuit, though it's also possible to simulate specific noise profiles. For now, we'll use the simplest noiseless simulator.
When running a quantum circuit on real hardware, qubits are initialized, gates are executed, and measurements provide results that are stored in a classical register. Quantum measurements are probabilistic due to superposition, meaning outcomes can't be predicted for single measurements. Instead, we calculate expected probability distributions over multiple runs, or "shots." The result of interest is the count, which shows how many times each possible state is observed.
from qiskit.primitives import StatevectorSampler
sampler = StatevectorSampler()
nshots = 1024
pub = (circuit)
job = sampler.run([pub], shots=nshots)
result = job.result()[0]
For this simple job, there is only one result, so [0]
grabs the only part there. In more complicated jobs, this index would cycle through different results.
Only expectation values are deterministic.
By default, "executing" a circuit will actually run the circuit 1024 times, for a single qubit, we only get "0" or "1" as possibilities.
result.data.c.get_counts()
{'0': 1024}
The c
label refers to the classical register.
That output means that in each of the 1024 shots, the system was observed in the state $|0\rangle$ . This is not surprising, because all we did was to create a bunch of these states and then measure them.
Now let's see what happens on a real quantum machine.
from qiskit_ibm_runtime import QiskitRuntimeService
#First time, use this:
service = QiskitRuntimeService(channel="ibm_quantum", token="MY_IBM_QUANTUM_TOKEN")
#overwrite = True
#Subsequent times (or in future notebooks) you can use this:
#service = QiskitRuntimeService()
backend = service.least_busy(simulator=False, operational=True)
backend.name
'ibm_kyiv'
We have selected the "least-busy" device to minimize the queue time for the job. The channel above makes use of the IBM open-access plan
service.backends()
[<IBMBackend('ibm_brisbane')>, <IBMBackend('ibm_kyiv')>, <IBMBackend('ibm_sherbrooke')>]
Once you have a backend, you must convert the circuit into a format appropriate for that machine's connectivity and instruction sets. This is referred to as "Instruction Set Architecure" (ISA). Additionally, it's a good idea to optimize the circuit by using Qiskit's knowledge of identities to reduce the number of gates as much as possible. optimization_level(0)/optimization_level(3)(default)
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
isa_circuit = pm.run(circuit)
Now you're ready to execute!
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
sampler = Sampler(backend)
job = sampler.run([(isa_circuit)],shots=nshots)
result = job.result()
job = service.job('cytxzcdq5bwg0083npm0')
job_result = job.result()
counts = job_result[0].data.c.get_counts()
Once the job has run, take a look at the results.
from qiskit.visualization import plot_histogram
plot_histogram(counts)
counts
{'0': 1024}
Woah! We have got the result.
However it's likely that even when preparing spins in the "up" state, we can observe some being measured in the "down" state.
Although quantum machines are cooled to millikelvin temperatures and isolated from their environment, errors still occur, even in simple circuits. The larger the circuit, the more errors tend to arise.
Qiskit Runtime routes all jobs through its system to automatically incorporate error correction.
so let’s move on to something more interesting.
Quantum Gates¶
2. Measuring observables ($S_x)$¶
What if we want to measure $ \hat{S}_x $ Instead of $ \hat{S}_z $?
In quantum mechanics, when we measure a qubit, we are typically measuring an operator like $ \hat{S}_z $, which corresponds to the computational basis (0
or 1
). And in practice, we can only measure in the computational basis (|0⟩ or |1⟩) only. However, to measure $ \hat{S}_x $, we rotate the qubit so that the new z-axis is aligned with the original x-axis. By measuring $ \hat{S}_z $ in the rotated state, we effectively measure $ \hat{S}_x $ on the original state. A great explanation of this strategy can be found here.
To rotate the system, we apply quantum gates that act as unitary operators to change the qubit's state. For instance:
- The X gate rotates the qubit in such a way that it corresponds to measuring $ \hat{S}_x $.
- Similarly, the Y and Z gates correspond to $ \hat{S}_y $ and $ \hat{S}_z $, respectively.
There are specific rotation gates that allow us to rotate the qubit state about the $x$, $y$, and $z$ axes:
- $ R_x(\theta),R_y(\theta),R_z(\theta) $ — rotates the qubit about the $x,y,z$-axis by an angle $\theta$.
The key idea is to rotate the system such that the desired axis of measurement aligns with the (z)-axis.
To measure the spin along a direction defined by a unit vector $\hat{n}(\theta, \phi)$, we first need to rotate the qubit so that $\hat{n}(\theta, \phi)$ aligns with the $z$-axis. This can be done using two rotation gates:
- Rotate by $-\phi$ about the $z$-axis: This aligns the direction $\hat{n}(\theta, \phi)$ to lie in the $x$-$z$ plane.
- Rotate by $-\theta$ about the $y$-axis: This final rotation brings the unit vector $\hat{n}(\theta, \phi)$ into perfect alignment with the $z$-axis.
After these rotations, we can measure the qubit in the computational basis ($\hat{S}_z$), which effectively corresponds to measuring along the original direction $\hat{n}(\theta, \phi)$.
To measure spin along the $x$ axis ($\theta = \frac{\pi}{2}$, $\phi = 0$) we simply need to rotate the system by $-\frac{\pi}{2}$ about the $y$ axis to map spin up/down (in this direction) to the computational basis (eigenstates of $\hat{S}_{z}$). The rotation gates can be accessed as rx(angle,qubit)
, rx(angle,qubit)
, rx(angle,qubit)
, where angle
is the desired rotation angle and qubit
specifies which qubit to rotate.
circuit2 = QuantumCircuit(qreg_q, creg_c)
circuit2.ry(-np.pi/2,0)
circuit2.measure(qreg_q,creg_c)
circuit2.draw('mpl')
So this circuit should simulate measuring $\hat{S}_{z}$ on the state $\left|+\right\rangle$. As you know, you cannot calculate what a given measurement should return. But you can calculate expectation values. This term is terribly misleading because we know that any individual measurement should return an eigenvalue of the operator being measured. The expectation value is simply the expected average of a large number of measurements repeated on identically prepared systems. What do you expect for $\left\langle \hat{S}_{x}\right\rangle$?
Let's see what the quantum computer gives! For efficiency, we'll make use of the simulator. But you could easily use the template above to try this on an actual device.
sampler = StatevectorSampler()
pub = (circuit2)
job = sampler.run([pub], shots=nshots)
# Extract the result for the 0th pub (this example only has one pub).
result = job.result()[0]
counts_sim = result.data.c.get_counts()
counts_sim
{'1': 478, '0': 546}
Even the simulator results will differ slightly from case to case due to statistical fluctuations, but you should see roughly 50/50 split between the state being $\left|0\right\rangle$ and $\left|1\right\rangle$. Recall that we rotated the system before the measurement, so we now interpret $\left|0\right\rangle\rightarrow \left|+\right\rangle_{x}$ and $\left|1\right\rangle \rightarrow \left|-\right\rangle_{x}$ (instead of $\left|+\right\rangle$ and $\left|-\right\rangle$, respectively. This 50/50 split is consistent with $\left\langle \hat{S}_{x}\right\rangle = 0$, as one would expect for $\left|\psi\right\rangle = \left|+\right\rangle$.
from qiskit.visualization import plot_histogram
plot_histogram(counts_sim, title="Simulator")
3. Generating arbitrary states¶
So far, we have seen how to perform basic measurements of spin about any arbitrary direction. But we always begin with the same initial state, $\left|0\right\rangle \rightarrow \left|+\right\rangle$. Is it possible to generate other states?
Absolutely. I will note that we could just be really clever and show how what we have developed can be used directly to measure any component of spin for any initial state. We don't actually need to generate a state and rotate the desired measurement direction into the computational basis. But sometimes one operation is more convenient than the other. And for the case of multiple qubits, it's convenient to think of state initialization and measurement rotations as two separate processes.
So let's begin!
Mathematically, how does one generate the state $\alpha\left|+\right\rangle + \beta \left|-\right\rangle$ from the state $\left|+\right\rangle$? This is equivalent to generating a vector by the following operation
$$\left(\begin{array}{c} \alpha\\ \beta \end{array}\right) = \hat{U}\left(\begin{array}{c}1\\ 0\end{array}\right).$$
The question becomes: what is $\hat{U}$ and how can I represent $\hat{U}$ as a quantum gate? We have already encountered the rotation gates, but there are much more general gates we can employ. First, let us note that $\alpha$ and $\beta$ are rather restricted. To ensure the state is normalized, we have $\left|\alpha\right|^{2} + \left|\beta\right|^{2} = 1$. Additionally, the overall phase of a state is physically unobservable. That is, $e^{i\delta}\left|\psi\right\rangle$ is physically indistinguishable from $\left|\psi\right\rangle$. Putting these conditions together, we could parameterize the two complex numbers $\alpha$ and $\beta$ as two real numbers, $\theta$ and $\phi$,
$$\left(\begin{array}{c} \alpha\\ \beta \end{array}\right) \equiv \left(\begin{array}{c} \cos\frac{\theta}{2}\\ e^{i\phi}\sin\frac{\theta}{2} \end{array}\right)$$
As it happens, the most general single-qubit gate corresponds to the following operator:
$$\hat{U}(\theta,\phi,\lambda) \;\;\dot{=} \left(\begin{array}{cc} \cos\frac{\theta}{2} & -e^{i\lambda}\sin\frac{\theta}{2} \\ e^{i\phi}\sin\frac{\theta}{2} & e^{i(\lambda + \phi)}\cos\frac{\theta}{2}\end{array}\right)$$
Here I use the symbol $\dot{=}$ to make a distinction between the physical gate used in a real quantum computer to change the physical state of the system and the mathematical represenation of states as vectors and operators as matrices. You should verify that $\hat{U}\left|+\right\rangle$ does return the state $\cos\frac{\theta}{2}\left|+\right\rangle + e^{i\phi}\sin\frac{\theta}{2}\left|-\right\rangle$.
Technical aside: This gate is referred to as a $\hat{U}_{3}(\theta,\phi,\lambda)$ gate in the Qiskit documentation due to its dependence on three parameters. There also exist gates $\hat{U}_{2}(\phi,\lambda) \equiv \hat{U}_{3}\left(\frac{\pi}{2},\phi,\lambda\right)$ and $\hat{U}_{1}(\lambda) \equiv \hat{U}_{3}\left(0,0,\lambda\right)$.
The proper syntax for adding a $\hat{U}$ gate to act on qubit q
in some circuit mycirc
is mycirc.u(theta,phi,lambda,q)
. Here are some optional exercises you can complete by copying, pasting, and lightly modifying statements above.
Exercise: Use what you know to generate the state $\left|+\right\rangle_{y} = \frac{1}{\sqrt{2}}\left(\left|+\right\rangle + i\left|-\right\rangle\right)$ and measure $\hat{S}_{x}$, $\hat{S}_{y}$, and $\hat{S}_{z}$. Note that based on what we've talked about, you'll need to design three separate circuits. Make sure to get simulator data and actual results. Before doing any programming, work out the values of $\theta$ and $\phi$ that you need (you can set $\lambda = 0$).
Also note: to measure $\hat{S}_{y}$, you'll have to perform two rotations before performing the measurement in the computational basis.
circuitsx = QuantumCircuit(qreg_q,creg_c)
circuitsx.u(np.pi/2,np.pi/2,0,0)
circuitsx.ry(-np.pi/2,0)
circuitsx.measure(qreg_q,creg_c)
circuitsx.draw('mpl')
circuitsy = QuantumCircuit(qreg_q,creg_c)
circuitsy.u(np.pi/2,np.pi/2,0,0)
circuitsy.rz(-np.pi/2,0)
circuitsy.ry(-np.pi/2,0)
circuitsy.measure(qreg_q,creg_c)
circuitsy.draw('mpl')
circuitsz = QuantumCircuit(qreg_q,creg_c)
circuitsz.u(np.pi/2,np.pi/2,0,0)
circuitsz.measure(qreg_q,creg_c)
circuitsz.draw('mpl')
sampler = StatevectorSampler()
pub = (circuit2)
job = sampler.run([pub], shots=nshots)
# Extract the result for the 0th pub (this example only has one pub).
result = job.result()[0]
counts_sim = result.data.c.get_counts()
samples = StatevectorSampler()
pubx = (circuitsx)
puby = (circuitsy)
pubz = (circuitsz)
jobx = sampler.run([pubz], shots=nshots)
joby = sampler.run([puby], shots=nshots)
jobz = sampler.run([pubz], shots=nshots)
resultx = jobx.result()[0]
resulty = joby.result()[0]
resultz = jobz.result()[0]
counts_sx = resultx.data.c.get_counts()
counts_sy = resulty.data.c.get_counts()
counts_sz = resultz.data.c.get_counts()
counts_sx
{'0': 492, '1': 532}
counts_sy
{'0': 1024}
counts_sz
{'0': 522, '1': 502}
Note that we created an eigenstate of $\hat{S}_{y}$, so all realizations had the same value $\left(+\frac{\hbar}{2}\right)$. But this state is a linear combination of the eigenstates of $\hat{S}_{x}$ or those of $\hat{S}_{z}$. The measured values of $\pm \frac{\hbar}{2}$ occur with roughly equal frequency for these two operators. Increasing the number of shots should show these measurements approaching a 50/50 split. We can actually increase the number of shots taken as follows:
samples = StatevectorSampler()
nshots = 8192
pubx = (circuitsx)
puby = (circuitsy)
pubz = (circuitsz)
jobx = sampler.run([pubz], shots=nshots)
joby = sampler.run([puby], shots=nshots)
jobz = sampler.run([pubz], shots=nshots)
resultx = jobx.result()[0]
resulty = joby.result()[0]
resultz = jobz.result()[0]
counts_sx = resultx.data.c.get_counts()
counts_sy = resulty.data.c.get_counts()
counts_sz = resultz.data.c.get_counts()
counts_sx
{'1': 4137, '0': 4055}
counts_sy
{'0': 8192}
counts_sz
{'1': 4019, '0': 4173}
For actual quantum hardware, we would expect similar (but perhaps noisier) results.
Standard Error Estimation: The histogram is a convenient means of visualizing the probabilities. But we can also quantify a statistical uncertainty from these measurements. Such an uncertainty is based on the variance of the measurements. The effective uncertainty $\delta S^{z}$ is given by the standard error of the mean
$$\delta S_{z} = \frac{\sigma_{S_{z}}}{\sqrt{N}},$$
where $\sigma_{S_{z}}$ is the standard deviations in the measurements of $\hat{S}_{z}$. Thus, you can report the result as the expectation value, plus-or-minus some effective uncertainty. Calculate the expectation values explicitly for $\left\langle \hat{S}_{x}\right\rangle$, $\left\langle \hat{S}_{x}\right\rangle$, $\left\langle \hat{S}_{x}\right\rangle$ for the state $\left|+\right\rangle_{y} = \frac{1}{\sqrt{2}}\left(\left|+\right\rangle +i\left|-\right\rangle\right)$.
To accomplish this, you'll need to a little bit of Python dictionary gymnastics. Your counts are stored as a dictionary, meaning it's of the form:
{'0': 584, '1': 440}
You can retrieve actual counts by calling counts_sx['0']
(which would return 584
. The only hiccup is that in the case all the counts were (say) '1'
, counts_sx['0']
would return an error. So you might need to check for the case where only one key exists and manually add the other entry with zero counts. Something like this would work:
if (len(counts_sx)==1):
if (list(counts_sx.keys())[0]=='0'):
counts_sx['1']=0
else:
counts_sx['0']=0
Solution: Here we use the simulations for simplicity, but this could easily be performed on the quantum hardware data.
samples = StatevectorSampler()
nshots = 8192
pubx = (circuitsx)
puby = (circuitsy)
pubz = (circuitsz)
jobx = sampler.run([pubz], shots=nshots)
joby = sampler.run([puby], shots=nshots)
jobz = sampler.run([pubz], shots=nshots)
resultx = jobx.result()[0]
resulty = joby.result()[0]
resultz = jobz.result()[0]
counts_sx = resultx.data.c.get_counts()
counts_sy = resulty.data.c.get_counts()
counts_sz = resultz.data.c.get_counts()
Now let's clean up the dictionaries:
def clean_dictionary(dict):
if (len(dict)==1):
if (list(dict.keys())[0]=='0'):
dict['1']=0
else:
dict['0']=0
return dict
counts_sx = clean_dictionary(counts_sx)
counts_sy = clean_dictionary(counts_sy)
counts_sz = clean_dictionary(counts_sz)
From here it's straightforward to compute the averages and variances once we remember that a key of '0'
corresponds to $+\frac{1}{2}$ and '1'
corresponds to $-\frac{1}{2}$ (setting $\hbar = 1$, as discussed earlier). To use built-in functions, it's easy if we just reconstruct the actual measured values:
measurements_sx = 0.5*np.concatenate([np.ones(counts_sx['0']),-np.ones(counts_sx['1'])])
measurements_sy = 0.5*np.concatenate([np.ones(counts_sy['0']),-np.ones(counts_sy['1'])])
measurements_sz = 0.5*np.concatenate([np.ones(counts_sz['0']),-np.ones(counts_sz['1'])])
Sxavg = np.mean(measurements_sx)
Syavg = np.mean(measurements_sy)
Szavg = np.mean(measurements_sz)
dSx = np.sqrt(np.var(measurements_sx)/nshots)
dSy = np.sqrt(np.var(measurements_sy)/nshots)
dSz = np.sqrt(np.var(measurements_sz)/nshots)
print('<Sx> = '+ str(Sxavg) + '+/-' + str(dSx))
print('<Sy> = '+ str(Syavg) + '+/-' + str(dSy))
print('<Sz> = '+ str(Szavg) + '+/-' + str(dSz))
<Sx> = 0.0040283203125+/-0.005524092436368126 <Sy> = 0.5+/-0.0 <Sz> = 0.0079345703125+/-0.005523576096585437
No surprises when we compare this to the histogram. But here you can see explicitly that $\left\langle \hat{S}_{x} \right\rangle \approx \left\langle \hat{S}_{z} \right\rangle \approx 0$ with comparable uncertainties. Since we began with an eigenstate of $\hat{S}_{y}$, this quantity is unambiguously defined, and we get no experimental uncertainty.
Introduction to the Estimator primitive
Suppose we wish to measure the three spin components on an actual quantum device. As we've formulated the measurements, we actually have three circuits. This absolutely could be done, but it's going to be cumbersome. It's also not great for efficiency to have three separate jobs that are to be run individually. Aside from the inconvenience of waiting on long queue times, it's always best to do as many related tasks as possible at once. The devices are calibrated daily, and there could be a noticeable sense "drift" over the timescales required to wait for these jobs to complete.
We have been making use of the basic Sampler primitive in which we run a circuit and get some bare counts. Spin expectation values were constructed through postprocessing of those counts ("we'll do it in post!"). The Estimator primitive is another way to "run" a quantum circuit in which expectation values are returned more efficiently.
Let's see how this works in practice. We don't really care about the invididual counts as much as the expectation value(s) computed from them. So let us consider the simpler circuit which just creates the state $|+\rangle_{y}$ and does nothing more.
circuit3 = QuantumCircuit(qreg_q,creg_c)
circuit3.u(np.pi/2,np.pi/2,0,0)
circuit3.draw('mpl')
We can actually make use of built-in representations for the Pauli matrices to define operators for the spin components. This is particularly handy for multi-qubit systems where correlation functions can involve quite a bit of postprocessing to obtain from raw counts.
We'll define operators for each component as $\hat{S}_{\alpha} = \frac{1}{2}\hat{\sigma}_{\alpha}$ (working in units of $\hbar$, or doing the nasty theorist thing of $\hbar\rightarrow 1$).
from qiskit.quantum_info import SparsePauliOp
Sx = SparsePauliOp.from_list([("X", 0.5)])
Sy = SparsePauliOp.from_list([("Y", 0.5)])
Sz = SparsePauliOp.from_list([("Z", 0.5)])
It's shockingly simple to use the StatevectorEstimator
to perform the desired measurements. Here the utility of the PUB becomes clearer. We lump our circuit and a list of the observables into the PUB and feed this to the estimator as a job. The execution is quite similar to what we did with the sampler.
from qiskit.primitives import StatevectorEstimator
estimator = StatevectorEstimator()
pub = (
circuit3, # circuit
[[Sx], [Sy], [Sz]], # Observables
)
job_result = estimator.run(pubs=[pub]).result()
Here's the payoff. We just peel off the expectation values of those operators from the job data. No more dictionary gymnastics required!
sx_est = job_result[0].data.evs[0][0]
sy_est = job_result[0].data.evs[1][0]
sz_est = job_result[0].data.evs[2][0]
You can even extract estimates for standard deviation. This isn't particularly useful for the simulator (they all compute to zero), but it'll be helpful for more complicated circuits on real hardware.
dsx = job_result[0].data.stds[0][0]
dsy = job_result[0].data.stds[1][0]
dsz = job_result[0].data.stds[2][0]
print('<Sx> = '+ str(sx_est) + '+/-' + str(dsx))
print('<Sy> = '+ str(sy_est) + '+/-' + str(dsy))
print('<Sz> = '+ str(sz_est) + '+/-' + str(dsy))
<Sx> = 3.061616997868383e-17+/-0.0 <Sy> = 0.5+/-0.0 <Sz> = 1.1102230246251565e-16+/-0.0
Wasn't that much easier? Check out how easy it is to do all this on a real device. The only new step is that the operators themselves have to be converted to appropriate ISA format for the given device.
backend = service.least_busy(simulator=False, operational=True)
backend.name
'ibm_kyiv'
pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
isa_circuit = pm.run(circuit3)
isa_Sx = Sx.apply_layout(layout=isa_circuit.layout)
isa_Sy = Sy.apply_layout(layout=isa_circuit.layout)
isa_Sz = Sz.apply_layout(layout=isa_circuit.layout)
from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2 as Estimator
estimator = Estimator(backend)
job = estimator.run([(isa_circuit,[[isa_Sx], [isa_Sy], [isa_Sz]])])
result = job.result()
from qiskit_ibm_runtime import QiskitRuntimeService
job = service.job('cyty4c14raf0008ejk0g')
job_result = job.result()
sx_est = job_result[0].data.evs[0][0]
sy_est = job_result[0].data.evs[1][0]
sz_est = job_result[0].data.evs[2][0]
dsx = job_result[0].data.stds[0][0]
dsy = job_result[0].data.stds[1][0]
dsz = job_result[0].data.stds[2][0]
print('<Sx> = '+ str(sx_est) + '+/-' + str(dsx))
print('<Sy> = '+ str(sy_est) + '+/-' + str(dsy))
print('<Sz> = '+ str(sz_est) + '+/-' + str(dsy))
<Sx> = -0.008136094674556213+/-0.006405415073867861 <Sy> = 0.5019723865877712+/-0.0018053558492237074 <Sz> = 0.0019723865877712033+/-0.0018053558492237074
One loses some low-level control by using the Estimator instead of the Sampler, but the gains in efficiency are pretty remarkable.