Bra-Ket Notation Explained
A practical guide to Dirac bra-ket notation as used in quantum computing: kets, bras, inner products, outer products, and how to read quantum circuit diagrams.
Bra-ket notation, also called Dirac notation, is the standard way physicists and quantum computing researchers write quantum states and operations. Once you know it, you can read quantum computing papers, textbooks, and circuit descriptions fluently. This tutorial teaches you the notation from scratch with concrete examples and runnable Python code.
Why Physicists Invented This Notation
Standard matrix notation works fine for small linear algebra problems, but it becomes awkward for quantum mechanics. In quantum mechanics you constantly need to distinguish between three types of objects: column vectors (quantum states), row vectors (dual states), and matrices (operators). In plain matrix notation, you have to keep track of which is which by context or by writing explicit transpose symbols everywhere.
Paul Dirac introduced bra-ket notation in 1939 to solve this problem. The notation makes the algebraic structure explicit at a glance:
|psi>is always a column vector (a quantum state)<psi|is always a row vector (the dual of a quantum state)<phi|psi>is always a scalar (an inner product, a probability amplitude)|psi><phi|is always a matrix (an outer product, an operator)
You can tell immediately what type of mathematical object you are looking at. There is no ambiguity. This matters because quantum mechanics chains these objects together in long expressions, and misinterpreting a scalar as a matrix (or vice versa) leads to nonsensical results.
Dirac notation has been the standard in physics for nearly a century, and quantum computing inherits it directly. Every quantum computing paper, every textbook, and every framework’s documentation uses it.
Kets: Quantum States as Column Vectors
A ket, written |psi>, represents a quantum state. The symbol inside the angle bracket is just a label; it can be a number, a letter, or a word. A ket is mathematically a column vector:
|0> = [1, 0]^T (the "zero" state)
|1> = [0, 1]^T (the "one" state)
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
# A general superposition state
alpha = 1 / np.sqrt(2)
beta = 1 / np.sqrt(2)
psi = alpha * ket_0 + beta * ket_1
# This is the |+> state: (|0> + |1>) / sqrt(2)
print(psi) # [0.70710678+0.j 0.70710678+0.j]
Any single-qubit state can be written as |psi> = alpha|0> + beta|1>, where alpha and beta are complex numbers called amplitudes. The amplitudes determine both the probabilities and the interference behavior of the qubit.
Complex Amplitudes and What They Mean
The amplitudes alpha and beta in |psi> = alpha|0> + beta|1> are complex numbers. Each complex number has a magnitude and a phase. The key rules are:
- Normalization:
|alpha|^2 + |beta|^2 = 1. The squared magnitudes must sum to 1 because they represent probabilities. - Born rule:
|alpha|^2is the probability of measuring 0, and|beta|^2is the probability of measuring 1. - Global phase is unobservable: Multiplying the entire state by a phase factor
e^(i*theta)produces no measurable difference. The states|psi>ande^(i*theta)|psi>are physically identical. - Relative phase matters: The relative phase between alpha and beta changes the physics. The states
|+> = (|0> + |1>)/sqrt(2)and|-> = (|0> - |1>)/sqrt(2)give the same probabilities when measured in the computational basis (50/50 for both), but they behave differently under gates and measurements in other bases.
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
# |+> and |-> have the same measurement probabilities in the Z basis
plus = (ket_0 + ket_1) / np.sqrt(2)
minus = (ket_0 - ket_1) / np.sqrt(2)
for name, state in [("|+>", plus), ("|->", minus)]:
prob_0 = abs(state[0])**2
prob_1 = abs(state[1])**2
print(f"{name}: P(0)={prob_0:.3f}, P(1)={prob_1:.3f}")
# |+>: P(0)=0.500, P(1)=0.500
# |->: P(0)=0.500, P(1)=0.500
# But they differ when measured in the X basis (the Hadamard basis)
# Measuring in X basis means projecting onto |+> and |->
# <+|+> = 1, <-|+> = 0: |+> always gives outcome +1 in X measurement
# <+|-> = 0, <-|-> = 1: |-> always gives outcome -1 in X measurement
print(f"<+|+> = {np.dot(plus.conj(), plus):.3f}") # 1.000
print(f"<-|+> = {np.dot(minus.conj(), plus):.3f}") # 0.000
print(f"<+|-> = {np.dot(plus.conj(), minus):.3f}") # 0.000
print(f"<-|-> = {np.dot(minus.conj(), minus):.3f}") # 1.000
Here is a state with a complex relative phase:
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
# |+i> = (|0> + i|1>) / sqrt(2)
plus_i = (ket_0 + 1j * ket_1) / np.sqrt(2)
prob_0 = abs(plus_i[0])**2
prob_1 = abs(plus_i[1])**2
print(f"|+i>: P(0)={prob_0:.3f}, P(1)={prob_1:.3f}")
# |+i>: P(0)=0.500, P(1)=0.500
# Same probabilities as |+> and |->, but this is a different state
# It is an eigenstate of the Y Pauli operator
Common Kets Table
Here are the quantum states you will encounter most often. Every vector below is normalized.
Single-qubit states
| Ket | Name | Vector | Notes |
|---|---|---|---|
|0> | Zero state | [1, 0] | Computational basis, Z eigenstate (+1) |
|1> | One state | [0, 1] | Computational basis, Z eigenstate (-1) |
|+> | Plus state | [1, 1] / sqrt(2) | X eigenstate (+1), Hadamard basis |
|-> | Minus state | [1, -1] / sqrt(2) | X eigenstate (-1), Hadamard basis |
|+i> | Plus-i state | [1, i] / sqrt(2) | Y eigenstate (+1) |
|-i> | Minus-i state | [1, -i] / sqrt(2) | Y eigenstate (-1) |
Bell states (two-qubit entangled states)
| Ket | Name | Vector |
|---|---|---|
|Phi+> | Bell state | [1, 0, 0, 1] / sqrt(2) = (|00> + |11>) / sqrt(2) |
|Phi-> | Bell state | [1, 0, 0, -1] / sqrt(2) = (|00> - |11>) / sqrt(2) |
|Psi+> | Bell state | [0, 1, 1, 0] / sqrt(2) = (|01> + |10>) / sqrt(2) |
|Psi-> | Bell state | [0, 1, -1, 0] / sqrt(2) = (|01> - |10>) / sqrt(2) |
The GHZ state (three-qubit entangled state)
| Ket | Name | Vector |
|---|---|---|
|GHZ> | GHZ state | [1, 0, 0, 0, 0, 0, 0, 1] / sqrt(2) = (|000> + |111>) / sqrt(2) |
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
# Single-qubit states
plus = (ket_0 + ket_1) / np.sqrt(2)
minus = (ket_0 - ket_1) / np.sqrt(2)
plus_i = (ket_0 + 1j * ket_1) / np.sqrt(2)
minus_i = (ket_0 - 1j * ket_1) / np.sqrt(2)
# Bell states
phi_plus = (np.kron(ket_0, ket_0) + np.kron(ket_1, ket_1)) / np.sqrt(2)
phi_minus = (np.kron(ket_0, ket_0) - np.kron(ket_1, ket_1)) / np.sqrt(2)
psi_plus = (np.kron(ket_0, ket_1) + np.kron(ket_1, ket_0)) / np.sqrt(2)
psi_minus = (np.kron(ket_0, ket_1) - np.kron(ket_1, ket_0)) / np.sqrt(2)
# GHZ state
ghz = (np.kron(np.kron(ket_0, ket_0), ket_0) +
np.kron(np.kron(ket_1, ket_1), ket_1)) / np.sqrt(2)
print("Bell states:")
print(f"|Phi+> = {phi_plus}")
print(f"|Phi-> = {phi_minus}")
print(f"|Psi+> = {psi_plus}")
print(f"|Psi-> = {psi_minus}")
print(f"\n|GHZ> = {ghz}")
Bras: The Dual of a Ket
A bra, written <psi|, is the conjugate transpose of the corresponding ket. If a ket is a column vector, the bra is a row vector with complex-conjugated entries:
<psi| = (|psi>)^dagger = conjugate transpose
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
def bra(ket):
return ket.conj()
bra_0 = bra(ket_0) # [1, 0]
bra_1 = bra(ket_1) # [0, 1]
# For complex amplitudes, conjugation changes the sign of imaginary parts
psi_complex = np.array([1/np.sqrt(2), 1j/np.sqrt(2)], dtype=complex)
bra_psi = bra(psi_complex)
print(f"|psi> = {psi_complex}")
print(f"<psi| = {bra_psi}")
# |psi> = [0.70710678+0.j 0. +0.70710678j]
# <psi| = [0.70710678-0.j 0. -0.70710678j]
The name “bra-ket” is a pun on “bracket”: a bra <phi| and ket |psi> placed together form a bracket <phi|psi>.
A critical detail: if |psi> = alpha|0> + beta|1>, then <psi| = alpha*<0| + beta*<1| where * denotes complex conjugation. The conjugation is not optional. Forgetting it produces wrong answers whenever the amplitudes have imaginary parts.
The Dagger (Conjugate Transpose)
The dagger symbol, written as a superscript cross (†), means “conjugate transpose.” It generalizes the bra-ket relationship to matrices:
- For a ket:
|psi>† = <psi| - For a bra:
<psi|† = |psi> - For a matrix:
(AB)† = B†A†(the order reverses, just like the transpose of a product) - Unitary matrices satisfy:
U†U = UU† = I(their dagger is their inverse) - Hermitian matrices satisfy:
H† = H(they equal their own dagger)
Note: in quantum computing, “H” for Hermitian and “H” for the Hadamard gate are different things. Context tells you which is which, but the overloaded notation confuses everyone at first.
import numpy as np
# The dagger of a matrix: conjugate transpose
# In NumPy: M.conj().T
# Pauli X is Hermitian (X† = X)
X = np.array([[0, 1], [1, 0]], dtype=complex)
print(f"X† == X? {np.allclose(X.conj().T, X)}") # True
# Hadamard is also Hermitian
H = (1/np.sqrt(2)) * np.array([[1, 1], [1, -1]], dtype=complex)
print(f"H† == H? {np.allclose(H.conj().T, H)}") # True
# Hadamard is also unitary: H†H = I
print(f"H†H == I? {np.allclose(H.conj().T @ H, np.eye(2))}") # True
# A non-Hermitian unitary example: the S gate
S = np.array([[1, 0], [0, 1j]], dtype=complex)
print(f"S† == S? {np.allclose(S.conj().T, S)}") # False
print(f"S†S == I? {np.allclose(S.conj().T @ S, np.eye(2))}") # True
# Product rule: (AB)† = B†A†
A = np.array([[1, 2j], [3, 4]], dtype=complex)
B = np.array([[0, 1j], [-1j, 2]], dtype=complex)
lhs = (A @ B).conj().T
rhs = B.conj().T @ A.conj().T
print(f"(AB)† == B†A†? {np.allclose(lhs, rhs)}") # True
Inner Products: Bra Times Ket
The inner product of bra <phi| with ket |psi> is written <phi|psi>. It is computed as the dot product of the conjugated bra vector with the ket vector:
<phi|psi> = phi^dagger . psi = sum_i phi_i* psi_i
The result is a complex number (a scalar). Its squared magnitude gives a probability.
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
plus = (ket_0 + ket_1) / np.sqrt(2)
def inner_product(phi, psi):
return np.dot(phi.conj(), psi)
# Orthonormal basis: <0|1> = 0, <0|0> = 1
print(inner_product(ket_0, ket_1)) # 0j (orthogonal)
print(inner_product(ket_0, ket_0)) # (1+0j) (normalized)
# General inner product
print(inner_product(ket_0, plus)) # (0.7071067811865475+0j)
The Born Rule
The Born rule says the probability of measuring outcome |x> from state |psi> is:
P(x) = |<x|psi>|^2
This is the absolute value squared of the inner product. It connects the abstract linear algebra to experimental predictions.
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
plus = (ket_0 + ket_1) / np.sqrt(2)
def inner_product(phi, psi):
return np.dot(phi.conj(), psi)
prob_0 = abs(inner_product(ket_0, plus))**2
prob_1 = abs(inner_product(ket_1, plus))**2
print(f"P(0) = {prob_0:.3f}, P(1) = {prob_1:.3f}") # 0.500, 0.500
# For a biased state
psi = np.array([np.sqrt(0.8), np.sqrt(0.2)], dtype=complex)
prob_0 = abs(inner_product(ket_0, psi))**2
prob_1 = abs(inner_product(ket_1, psi))**2
print(f"P(0) = {prob_0:.3f}, P(1) = {prob_1:.3f}") # 0.800, 0.200
Orthogonality and Orthonormality
Two states |phi> and |psi> are:
- Orthogonal if
<phi|psi> = 0. Orthogonal states are perfectly distinguishable when measured in a basis that contains both of them. - Normalized if
<psi|psi> = 1. All physical quantum states must be normalized. - Orthonormal if both conditions hold:
<phi_i|phi_j> = delta_ij(which equals 1 when i = j, and 0 otherwise).
The computational basis {|0>, |1>} is orthonormal. The Hadamard basis {|+>, |->} is also orthonormal. The Y eigenstates {|+i>, |-i>} are also orthonormal.
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
plus = (ket_0 + ket_1) / np.sqrt(2)
minus = (ket_0 - ket_1) / np.sqrt(2)
plus_i = (ket_0 + 1j * ket_1) / np.sqrt(2)
minus_i = (ket_0 - 1j * ket_1) / np.sqrt(2)
def inner_product(phi, psi):
return np.dot(phi.conj(), psi)
# Computational basis is orthonormal
print(f"<0|0> = {inner_product(ket_0, ket_0):.1f}") # 1.0
print(f"<0|1> = {inner_product(ket_0, ket_1):.1f}") # 0.0
print(f"<1|1> = {inner_product(ket_1, ket_1):.1f}") # 1.0
# Hadamard basis is orthonormal
print(f"<+|+> = {inner_product(plus, plus):.1f}") # 1.0
print(f"<+|-> = {inner_product(plus, minus):.1f}") # 0.0
print(f"<-|-> = {inner_product(minus, minus):.1f}") # 1.0
# Y eigenstates are orthonormal
print(f"<+i|+i> = {inner_product(plus_i, plus_i):.1f}") # 1.0
print(f"<+i|-i> = {inner_product(plus_i, minus_i):.1f}") # 0.0
Outer Products: Ket Times Bra
The outer product |psi><phi| is the reverse order: a ket times a bra. The result is a matrix, not a scalar:
|psi><phi| is a matrix with (i,j) entry = psi_i * phi_j*
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
def outer_product(psi, phi):
return np.outer(psi, phi.conj())
# Projector onto |0>
P0 = outer_product(ket_0, ket_0)
print("P0 =")
print(P0)
# [[1, 0],
# [0, 0]]
# Projector onto |1>
P1 = outer_product(ket_1, ket_1)
print("P1 =")
print(P1)
# [[0, 0],
# [0, 1]]
Projectors have a special property: applying them twice gives the same result as applying them once. That is, P^2 = P. You can verify this:
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
P0 = np.outer(ket_0, ket_0.conj())
print(np.allclose(P0 @ P0, P0)) # True
The Resolution of Identity
For any complete orthonormal basis {|e_i>}, the sum of all projectors onto those basis states equals the identity matrix:
sum_i |e_i><e_i| = I
This is called the “completeness relation” or “resolution of identity.” It is one of the most frequently used tools in quantum computing derivations.
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
plus = (ket_0 + ket_1) / np.sqrt(2)
minus = (ket_0 - ket_1) / np.sqrt(2)
# Resolution of identity from the computational basis
I_comp = np.outer(ket_0, ket_0.conj()) + np.outer(ket_1, ket_1.conj())
print(f"Z-basis: sum = I? {np.allclose(I_comp, np.eye(2))}") # True
# Also works for the X-basis (Hadamard basis)
I_x = np.outer(plus, plus.conj()) + np.outer(minus, minus.conj())
print(f"X-basis: sum = I? {np.allclose(I_x, np.eye(2))}") # True
# And for the Y-basis
plus_i = (ket_0 + 1j * ket_1) / np.sqrt(2)
minus_i = (ket_0 - 1j * ket_1) / np.sqrt(2)
I_y = np.outer(plus_i, plus_i.conj()) + np.outer(minus_i, minus_i.conj())
print(f"Y-basis: sum = I? {np.allclose(I_y, np.eye(2))}") # True
The resolution of identity is used constantly to expand states in different bases. If you want to express a state |psi> in terms of basis states {|e_i>}, insert the identity:
|psi> = I |psi> = sum_i |e_i><e_i|psi>
The coefficients <e_i|psi> are the amplitudes of |psi> in that basis.
Matrix Elements in Bra-Ket Notation
The expression <phi|A|psi> means: apply operator A to ket |psi>, then take the inner product with bra <phi|. This gives the component of A|psi> in the direction of |phi>:
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
def inner_product(phi, psi):
return np.dot(phi.conj(), psi)
H = (1/np.sqrt(2)) * np.array([[1, 1],
[1, -1]], dtype=complex)
# Matrix element <0|H|1>
element = inner_product(ket_0, H @ ket_1)
print(f"<0|H|1> = {element:.4f}") # 0.7071 (= 1/sqrt(2))
# Matrix element <1|H|1>
element = inner_product(ket_1, H @ ket_1)
print(f"<1|H|1> = {element:.4f}") # -0.7071 (= -1/sqrt(2))
Expectation Values
The expectation value of an observable A in state |psi> is written:
<A> = <psi|A|psi>
This is the “sandwich” form: bra on the left, operator in the middle, ket on the right. It gives the average value you would obtain if you measured observable A on many copies of |psi>.
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
plus = (ket_0 + ket_1) / np.sqrt(2)
X = np.array([[0, 1], [1, 0]], dtype=complex)
Z = np.array([[1, 0], [0, -1]], dtype=complex)
for name, state in [('|0>', ket_0), ('|1>', ket_1), ('|+>', plus)]:
exp_Z = np.real(state.conj() @ Z @ state)
exp_X = np.real(state.conj() @ X @ state)
print(f"{name}: <Z>={exp_Z:.2f}, <X>={exp_X:.2f}")
# |0>: <Z>=1.00, <X>=0.00
# |1>: <Z>=-1.00, <X>=0.00
# |+>: <Z>=0.00, <X>=1.00
The results make physical sense:
|0>is the +1 eigenstate of Z, so<Z> = 1. It is not an eigenstate of X, so<X> = 0(equal probability of +1 and -1 outcomes).|1>is the -1 eigenstate of Z, so<Z> = -1.|+>is the +1 eigenstate of X, so<X> = 1. It is not an eigenstate of Z, so<Z> = 0.
When the expectation value is 0, that does not mean the observable has value 0. It means the outcomes average to 0. For <Z> measured on |+>, you get +1 half the time and -1 half the time, which averages to 0.
Writing Operators in Bra-Ket Form
Any operator A can be written as a sum of outer products using a complete basis:
A = sum_ij A_ij |i><j| where A_ij = <i|A|j>
This representation is powerful because it shows you exactly what the operator does to each basis state.
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
plus = (ket_0 + ket_1) / np.sqrt(2)
minus = (ket_0 - ket_1) / np.sqrt(2)
# X = |0><1| + |1><0|
# This reads as: X maps |1> to |0> and maps |0> to |1> (a bit flip)
X_bra_ket = np.outer(ket_0, ket_1.conj()) + np.outer(ket_1, ket_0.conj())
X = np.array([[0, 1], [1, 0]], dtype=complex)
print(f"X from bra-ket == X matrix? {np.allclose(X_bra_ket, X)}") # True
# Z = |0><0| - |1><1|
# This reads as: Z leaves |0> unchanged and flips the sign of |1>
Z_bra_ket = np.outer(ket_0, ket_0.conj()) - np.outer(ket_1, ket_1.conj())
Z = np.array([[1, 0], [0, -1]], dtype=complex)
print(f"Z from bra-ket == Z matrix? {np.allclose(Z_bra_ket, Z)}") # True
# Hadamard: H = |+><0| + |-><1|
# This reads as: H maps |0> to |+> and maps |1> to |->
H_bra_ket = np.outer(plus, ket_0.conj()) + np.outer(minus, ket_1.conj())
H = (1/np.sqrt(2)) * np.array([[1, 1], [1, -1]], dtype=complex)
print(f"H from bra-ket == H matrix? {np.allclose(H_bra_ket, H)}") # True
Applying Operators Using Bra-Ket Algebra
One of the most useful skills in quantum computing is applying operators to states using bra-ket algebra instead of matrix multiplication. The key rule is that inner products between basis states collapse to 0 or 1:
<0|0> = 1, <0|1> = 0, <1|0> = 0, <1|1> = 1
Let us trace through two examples step by step.
Example 1: Apply H to |0>
Write H in bra-ket form: H = |+><0| + |-><1|
H|0> = (|+><0| + |-><1>)|0>
= |+><0|0> + |-><1|0>
= |+> * 1 + |-> * 0
= |+>
So H maps |0> to |+>, as expected.
Example 2: Apply X to |+>
Write X in bra-ket form: X = |0><1| + |1><0|
Write |+> explicitly: |+> = (|0> + |1>)/sqrt(2)
X|+> = (|0><1| + |1><0|)(|0> + |1>)/sqrt(2)
= (|0><1|0> + |0><1|1> + |1><0|0> + |1><0|1>) / sqrt(2)
= (|0>*0 + |0>*1 + |1>*1 + |1>*0) / sqrt(2)
= (|0> + |1>) / sqrt(2)
= |+>
So X|+> = |+>, confirming that |+> is an eigenstate of X with eigenvalue +1.
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
plus = (ket_0 + ket_1) / np.sqrt(2)
X = np.array([[0, 1], [1, 0]], dtype=complex)
H = (1/np.sqrt(2)) * np.array([[1, 1], [1, -1]], dtype=complex)
# Verify: H|0> = |+>
result = H @ ket_0
print(f"H|0> = {result}")
print(f"Equals |+>? {np.allclose(result, plus)}") # True
# Verify: X|+> = |+>
result = X @ plus
print(f"X|+> = {result}")
print(f"Equals |+>? {np.allclose(result, plus)}") # True
Tensor Products and Multi-Qubit Kets
For multi-qubit systems, the labels in the ket represent all qubits at once. You can write multi-qubit states using several equivalent notations:
|0>|1> = |0> (x) |1> = |01>
These all mean the same thing: a two-qubit state where the first qubit is |0> and the second qubit is |1>. The tensor product symbol (x) is sometimes written as a circled cross.
The vector representation of a multi-qubit state comes from the Kronecker product of the individual qubit vectors.
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
# |01> as a vector
ket_01 = np.kron(ket_0, ket_1)
print(f"|01> = {ket_01}") # [0, 1, 0, 0]
# |10> as a vector
ket_10 = np.kron(ket_1, ket_0)
print(f"|10> = {ket_10}") # [0, 0, 1, 0]
Reading Multi-Qubit State Vectors
For a 3-qubit state like |101>: qubit 0 (leftmost) is in state |1>, qubit 1 (middle) is in state |0>, qubit 2 (rightmost) is in state |1>. The number 101 in binary equals 5 in decimal, so the vector has a 1 at index 5:
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
ket_101 = np.kron(np.kron(ket_1, ket_0), ket_1)
print(f"|101> = {ket_101}")
# [0. 0. 0. 0. 0. 1. 0. 0.]
# The 1 is at index 5 = 101 in binary
The Endianness Problem
Different papers and frameworks use different conventions for which qubit is “first” in the tensor product. In the standard physics convention (used by most textbooks), the leftmost qubit is the most significant bit:
|q0 q1 q2> = |q0> (x) |q1> (x) |q2>
Qiskit reverses this convention. In Qiskit, the rightmost qubit has the lowest index:
Qiskit: q0 is rightmost, q2 is leftmost
So when Qiskit prints a state as |101>, qubit 0 is the rightmost 1, qubit 1 is the middle 0, and qubit 2 is the leftmost 1. This is the opposite of the physics convention.
Always check which convention your framework uses. Getting it wrong leads to silently incorrect results.
Entangled States
Not all multi-qubit states can be written as a tensor product of single-qubit states. The Bell state (|00> + |11>)/sqrt(2) cannot be factored into |a>|b> for any single-qubit states |a> and |b>. Such states are called entangled.
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
# Bell state: (|00> + |11>) / sqrt(2)
bell = (np.kron(ket_0, ket_0) + np.kron(ket_1, ket_1)) / np.sqrt(2)
print(f"|Bell> = {bell}") # [0.707, 0., 0., 0.707]
# This state is entangled: measuring qubit 0 as |0> forces qubit 1 to also be |0>
# and measuring qubit 0 as |1> forces qubit 1 to also be |1>
The Trace and Partial Trace
The trace of an operator is the sum of its diagonal elements, which can be written in bra-ket notation as:
Tr(A) = sum_i <i|A|i>
The trace has important properties:
Tr(I) = dwhere d is the dimension (2 for one qubit, 4 for two qubits, etc.)Tr(rho) = 1for any valid density matrix rhoTr(AB) = Tr(BA)(the cyclic property, used constantly in derivations)Tr(|psi><phi|) = <phi|psi>(the trace of an outer product equals the inner product)
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
plus = (ket_0 + ket_1) / np.sqrt(2)
# Tr(I) = 2 for a single-qubit system
I = np.eye(2, dtype=complex)
print(f"Tr(I) = {np.trace(I):.0f}") # 2
# Tr(rho) = 1 for a valid density matrix
# Density matrix of |+>: rho = |+><+|
rho_plus = np.outer(plus, plus.conj())
print(f"Tr(|+><+|) = {np.trace(rho_plus):.1f}") # 1.0
# Cyclic property: Tr(AB) = Tr(BA)
X = np.array([[0, 1], [1, 0]], dtype=complex)
Z = np.array([[1, 0], [0, -1]], dtype=complex)
print(f"Tr(XZ) = {np.trace(X @ Z):.1f}") # 0.0
print(f"Tr(ZX) = {np.trace(Z @ X):.1f}") # 0.0
# Trace of outer product = inner product: Tr(|0><1|) = <1|0> = 0
outer_01 = np.outer(ket_0, ket_1.conj())
print(f"Tr(|0><1|) = {np.trace(outer_01):.1f}") # 0.0
The Partial Trace (Brief Introduction)
For multi-qubit systems, the partial trace traces out (discards) some qubits, leaving a reduced density matrix for the remaining qubits. If you have a two-qubit state rho_AB and want to know the state of qubit A alone, you compute:
rho_A = Tr_B(rho_AB)
The partial trace is how you describe subsystems of entangled states. For the Bell state, tracing out either qubit gives the maximally mixed state I/2, meaning each qubit alone is completely random even though the two qubits together are in a pure state. This is the mathematical signature of entanglement.
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
# Bell state density matrix
bell = (np.kron(ket_0, ket_0) + np.kron(ket_1, ket_1)) / np.sqrt(2)
rho_bell = np.outer(bell, bell.conj())
# Partial trace over qubit B (the second qubit)
# For a 2x2 system, reshape to (2,2,2,2) and trace over axes 1,3
rho_reshaped = rho_bell.reshape(2, 2, 2, 2)
rho_A = np.trace(rho_reshaped, axis1=1, axis2=3)
print("rho_A (after tracing out qubit B):")
print(rho_A)
# [[0.5, 0. ],
# [0. , 0.5]]
# This is I/2: the maximally mixed state
Reading Quantum Circuit Diagrams
In a quantum circuit diagram, wires are qubits and boxes are gates. Bra-ket notation appears in several places:
- Initial state: qubits usually start in
|0> - Gate labels: H for Hadamard, CNOT with a control dot and target circle
- Measurement: the meter symbol collapses the qubit to a classical bit
- Output state: written as a ket or as a sum of kets with amplitudes
A circuit that prepares the Bell state (|00> + |11>) / sqrt(2):
from qiskit import QuantumCircuit
qc = QuantumCircuit(2)
qc.h(0) # H gate on qubit 0: |0> -> |+>
qc.cx(0, 1) # CNOT: control qubit 0, target qubit 1
print(qc.draw())
After the Hadamard on qubit 0, the state is |+>|0> = (|00> + |10>)/sqrt(2). After the CNOT, the state is (|00> + |11>)/sqrt(2). The CNOT flips qubit 1 whenever qubit 0 is |1>.
You can trace through this using bra-ket algebra. The CNOT operator in bra-ket form is:
CNOT = |0><0| (x) I + |1><1| (x) X
This reads as: “If the control qubit is |0>, do nothing to the target. If the control qubit is |1>, apply X to the target.”
Common Mistakes
Here are errors that trip up beginners and sometimes experienced practitioners too.
Confusing outer products with inner products
|0><1| is an outer product (a 2x2 matrix). <0|1> is an inner product (a scalar equal to 0). The symbols look similar but represent completely different mathematical objects.
import numpy as np
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
# Outer product: a matrix
outer = np.outer(ket_0, ket_1.conj())
print(f"|0><1| =\n{outer}")
# [[0, 1],
# [0, 0]]
# Inner product: a scalar
inner = np.dot(ket_0.conj(), ket_1)
print(f"<0|1> = {inner}") # 0
Forgetting complex conjugation in bras
If |psi> = alpha|0> + beta|1>, then <psi| = alpha*<0| + beta*<1| where * denotes the complex conjugate. This matters whenever the amplitudes have imaginary parts.
import numpy as np
# State with complex amplitudes
psi = np.array([1/np.sqrt(2), 1j/np.sqrt(2)], dtype=complex)
# WRONG: forgetting conjugation
wrong_norm = np.dot(psi, psi)
print(f"Wrong (no conjugation): {wrong_norm:.4f}") # 0.0000 (not 1!)
# RIGHT: with conjugation
right_norm = np.dot(psi.conj(), psi)
print(f"Right (with conjugation): {right_norm:.4f}") # 1.0000
Unnormalized states in expectation values
The formula <A> = <psi|A|psi> assumes <psi|psi> = 1. If your state is not normalized, you must divide by the norm:
<A> = <psi|A|psi> / <psi|psi>
import numpy as np
Z = np.array([[1, 0], [0, -1]], dtype=complex)
# An unnormalized state
psi_unnorm = np.array([3, 4], dtype=complex)
norm_sq = np.dot(psi_unnorm.conj(), psi_unnorm)
print(f"<psi|psi> = {norm_sq:.1f}") # 25.0 (not 1!)
# Wrong expectation value (without normalization)
wrong = np.real(psi_unnorm.conj() @ Z @ psi_unnorm)
print(f"Wrong <Z> = {wrong:.1f}") # -7.0
# Correct expectation value (with normalization)
correct = wrong / norm_sq
print(f"Correct <Z> = {correct:.4f}") # -0.2800
Endianness confusion in multi-qubit kets
In the standard physics convention, |01> means qubit 0 is |0> and qubit 1 is |1>. In Qiskit’s output strings, the order is reversed: the rightmost character corresponds to qubit 0. If you see Qiskit report the state "01", that means qubit 0 measured 1 and qubit 1 measured 0.
Orthogonality does not mean distinguishable in every basis
If <phi|psi> = 0, the states |phi> and |psi> are perfectly distinguishable when measured in a basis that contains both of them. But they may not be distinguishable in other bases. For example, |0> and |1> are orthogonal and distinguishable by Z measurement, but a single X measurement cannot reliably distinguish them (both give +1 and -1 with equal probability).
Summary Table
| Expression | Type | Meaning |
|---|---|---|
|psi> | Ket (column vector) | Quantum state |
<psi| | Bra (row vector) | Dual of ket (conjugate transpose) |
<phi|psi> | Scalar | Inner product; probability amplitude |
|psi><phi| | Matrix | Outer product; projector or operator |
<phi|A|psi> | Scalar | Matrix element of operator A |
<psi|A|psi> | Scalar | Expectation value of A in state psi |
A† | Matrix | Conjugate transpose (dagger) of A |
Tr(A) | Scalar | Trace of operator A |
|a>|b> | Ket (tensor product) | Multi-qubit state |
sum_i |i><i| | Matrix (= I) | Resolution of identity |
Next Steps
- Linear Algebra for Quantum Computing, covering the full linear algebra behind the notation
- Quantum Measurement Explained, covering how
<phi|psi>connects to measurement - Quantum Gates Explained, with standard gates written in bra-ket form
Was this tutorial helpful?