Linear Algebra for Quantum Computing: Vectors, Matrices, and Inner Products
Master the linear algebra underlying quantum computing: Dirac notation, inner products, tensor products, Hermitian and unitary matrices, eigenvalues, and partial trace, all with numpy examples.
Quantum computing is applied linear algebra. The mathematical objects of quantum mechanics (states, gates, measurements) are vectors and matrices in complex vector spaces. This tutorial builds the mathematical foundation you need to read quantum computing literature and understand what is actually happening in quantum circuits.
Setup
import numpy as np
Dirac Notation: Bras, Kets, and Brackets
Physicists use bra-ket notation (Dirac notation) instead of standard vector notation. It is compact and expressive once you learn it.
Kets: Column Vectors
A ket |ψ⟩ is a column vector. The two computational basis kets are:
|0⟩ = [1, 0]ᵀ (column vector)
|1⟩ = [0, 1]ᵀ
ket_0 = np.array([[1], [0]], dtype=complex) # Column vector
ket_1 = np.array([[0], [1]], dtype=complex)
# Or as 1D arrays (more common in practice)
ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)
A general qubit state is a linear combination:
|ψ⟩ = α|0⟩ + β|1⟩
alpha = 1/np.sqrt(2)
beta = 1/np.sqrt(2)
psi = alpha * ket_0 + beta * ket_1
print(psi) # [0.707+0j, 0.707+0j]
Bras: Row Vectors (Conjugate Transposes)
A bra ⟨ψ| is the conjugate transpose of the corresponding ket:
⟨ψ| = (|ψ⟩)† = conjugate transpose
def bra(ket):
return ket.conj() # For 1D arrays; use .conj().T for 2D
bra_0 = bra(ket_0) # [1, 0]
bra_1 = bra(ket_1) # [0, 1]
# For complex states, conjugation matters
psi_complex = np.array([1/np.sqrt(2), 1j/np.sqrt(2)], dtype=complex)
bra_psi = psi_complex.conj()
print(bra_psi) # [0.70710678-0.j 0. -0.70710678j] (note: i becomes -i)
Inner Products
The inner product ⟨φ|ψ⟩ (a “bracket”) is computed as:
⟨φ|ψ⟩ = φ† · ψ = Σ φᵢ* ψᵢ
The inner product of a state with itself gives the squared norm (which must equal 1 for a valid quantum state):
def inner_product(phi, psi):
return np.dot(phi.conj(), psi)
# Normalization check
print(inner_product(psi, psi).real) # 1.0 (valid quantum state)
# Orthogonality of basis states
print(inner_product(ket_0, ket_1)) # 0j (orthogonal)
print(inner_product(ket_0, ket_0)) # (1+0j) (normalized)
Born Rule: Probabilities from Inner Products
The probability of measuring outcome |x⟩ from state |ψ⟩ is:
P(x) = |⟨x|ψ⟩|²
# State: equal superposition
psi = np.array([1/np.sqrt(2), 1/np.sqrt(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.500, 0.500
Outer Products and Density Matrices
The outer product |ψ⟩⟨φ| is a matrix:
def outer_product(psi, phi):
return np.outer(psi, phi.conj())
# Projector onto |0⟩
P0 = outer_product(ket_0, ket_0)
print(P0)
# [[1, 0],
# [0, 0]]
# Projector onto |1⟩
P1 = outer_product(ket_1, ket_1)
# Completeness: P0 + P1 = I
print(P0 + P1) # [[1, 0], [0, 1]] = Identity
Density Matrices
A density matrix ρ = |ψ⟩⟨ψ| represents a pure quantum state as a matrix. It is useful for mixed states (statistical mixtures of pure states):
# Pure state density matrix
psi = np.array([1/np.sqrt(2), 1/np.sqrt(2)], dtype=complex)
rho = outer_product(psi, psi)
print(rho)
# [[0.5, 0.5],
# [0.5, 0.5]]
# Purity: Tr(ρ²) = 1 for pure states
purity = np.trace(rho @ rho).real
print(f"Purity: {purity:.3f}") # 1.000
# Mixed state: 50/50 mixture of |0⟩ and |1⟩
rho_mixed = 0.5 * outer_product(ket_0, ket_0) + 0.5 * outer_product(ket_1, ket_1)
purity_mixed = np.trace(rho_mixed @ rho_mixed).real
print(f"Mixed purity: {purity_mixed:.3f}") # 0.500
Tensor Products
The tensor product (Kronecker product) combines vector spaces. For two qubit states, the two-qubit system lives in the tensor product space:
# Two-qubit basis states
ket_00 = np.kron(ket_0, ket_0) # [1, 0, 0, 0]
ket_01 = np.kron(ket_0, ket_1) # [0, 1, 0, 0]
ket_10 = np.kron(ket_1, ket_0) # [0, 0, 1, 0]
ket_11 = np.kron(ket_1, ket_1) # [0, 0, 0, 1]
print(ket_01) # [0, 1, 0, 0]
Extending Gates to Multi-Qubit Systems
To apply gate G to qubit 0 in a 2-qubit system, tensor G with the identity I:
I = np.eye(2, dtype=complex)
H = (1/np.sqrt(2)) * np.array([[1, 1], [1, -1]], dtype=complex)
# H on qubit 0, I on qubit 1: H ⊗ I
H_on_0 = np.kron(H, I)
# I on qubit 0, H on qubit 1: I ⊗ H
H_on_1 = np.kron(I, H)
print(H_on_0.shape) # (4, 4)
Entangled States Cannot Be Factored
A product state can be written as a tensor product. An entangled state cannot:
# Product state: |+⟩ ⊗ |0⟩
plus = np.array([1/np.sqrt(2), 1/np.sqrt(2)], dtype=complex)
product_state = np.kron(plus, ket_0)
print(product_state) # [0.70710678+0.j 0. +0.j 0.70710678+0.j 0. +0.j]
# Bell state: (|00⟩ + |11⟩)/√2 -- cannot be factored
bell = np.array([1/np.sqrt(2), 0, 0, 1/np.sqrt(2)], dtype=complex)
# No tensored form of this exists
Hermitian Matrices
A matrix A is Hermitian if A† = A (equal to its own conjugate transpose). Hermitian matrices represent observable quantities in quantum mechanics. Their eigenvalues are always real.
def is_hermitian(A, tol=1e-10):
return np.allclose(A, A.conj().T, atol=tol)
# Pauli matrices are Hermitian
X = np.array([[0, 1], [1, 0]], dtype=complex)
Y = np.array([[0, -1j], [1j, 0]], dtype=complex)
Z = np.array([[1, 0], [0, -1]], dtype=complex)
print(is_hermitian(X)) # True
print(is_hermitian(Y)) # True
print(is_hermitian(Z)) # True
Eigenvalues and Eigenvectors
The eigenvalues of a quantum observable are the possible measurement outcomes. Eigenvectors are the states with definite measurement values:
# Z gate eigenvalues and eigenvectors
eigenvalues, eigenvectors = np.linalg.eigh(Z)
print("Eigenvalues:", eigenvalues) # [-1. 1.]
print("Eigenvectors:\n", eigenvectors)
# Column 0 is |1⟩ (eigenvalue -1)
# Column 1 is |0⟩ (eigenvalue +1)
The Pauli Z observable has eigenvalues +1 (for |0⟩) and -1 (for |1⟩). Measuring in the Z basis means asking “are you |0⟩ or |1⟩?”
# Expectation value of Z on state |+⟩
plus = np.array([1/np.sqrt(2), 1/np.sqrt(2)], dtype=complex)
expectation_Z = np.real(plus.conj() @ Z @ plus)
print(f"<+|Z|+> = {expectation_Z:.3f}") # 0.000 (equal mixture of +1 and -1)
# Expectation value of Z on state |0⟩
expectation_Z0 = np.real(ket_0.conj() @ Z @ ket_0)
print(f"<0|Z|0> = {expectation_Z0:.3f}") # 1.000 (definite +1)
Unitary Matrices
A matrix U is unitary if U†U = UU† = I. Unitary matrices represent quantum gates. They preserve inner products (and therefore probabilities):
def is_unitary(U, tol=1e-10):
product = U.conj().T @ U
return np.allclose(product, np.eye(len(U)), atol=tol)
# All standard gates are unitary
S = np.array([[1, 0], [0, 1j]], dtype=complex)
T = np.array([[1, 0], [0, np.exp(1j * np.pi / 4)]], dtype=complex)
for name, gate in [("X", X), ("Y", Y), ("Z", Z), ("H", H), ("S", S), ("T", T)]:
print(f"{name}: unitary={is_unitary(gate)}")
# Unitary matrices preserve normalization
psi = np.array([3/5, 4/5], dtype=complex)
psi_after = H @ psi
print(np.linalg.norm(psi_after)) # Still 1.0
Relationship Between Hermitian and Unitary
A Hermitian matrix H generates a unitary via exponentiation:
U = e^(iθH)
This is how time evolution works in quantum mechanics and why parameterized gates like Rx(θ) = e^(-iθX/2) have their form:
def rx(theta):
"""Rotation around X axis by angle theta."""
return np.cos(theta/2) * np.eye(2, dtype=complex) - 1j * np.sin(theta/2) * X
gate = rx(np.pi/2)
print(is_unitary(gate)) # True
print(gate)
# [[0.707+0j, 0.-0.707j],
# [0.-0.707j, 0.707+0j ]]
Trace and Partial Trace
The trace of a matrix is the sum of its diagonal elements:
print(np.trace(X)) # 0j (X = [[0,1],[1,0]])
print(np.trace(Z)) # 0j (Z = [[1,0],[0,-1]])
print(np.trace(np.eye(2))) # 2.0
For density matrices, Tr(ρ) = 1 always (probability sums to 1).
Partial Trace: Tracing Out a Subsystem
The partial trace reduces a two-qubit density matrix to a single-qubit density matrix by “tracing out” one qubit. This is how you compute the reduced state of one qubit in an entangled pair:
def partial_trace_qubit1(rho_2q):
"""Trace out qubit 1 from a 2-qubit 4x4 density matrix."""
# Reshape: (2, 2, 2, 2)
rho = rho_2q.reshape(2, 2, 2, 2)
# Trace over qubit 1 (second index pair)
return np.einsum('ijik->jk', rho)
def partial_trace_qubit0(rho_2q):
"""Trace out qubit 0 from a 2-qubit 4x4 density matrix."""
rho = rho_2q.reshape(2, 2, 2, 2)
return np.einsum('ijkj->ik', rho)
# Bell state density matrix
bell = np.array([1/np.sqrt(2), 0, 0, 1/np.sqrt(2)], dtype=complex)
rho_bell = np.outer(bell, bell.conj())
print(rho_bell)
# Reduced state of qubit 0
rho_0 = partial_trace_qubit1(rho_bell)
print(rho_0)
# [[0.5, 0],
# [0, 0.5]] -- maximally mixed! (no information in single qubit of a Bell pair)
# Purity of reduced state
purity_0 = np.trace(rho_0 @ rho_0).real
print(f"Purity: {purity_0:.3f}") # 0.500 (mixed state = entangled)
The reduced state of a Bell pair is maximally mixed: this is the signature of maximal entanglement. When two qubits are maximally entangled, each individual qubit carries no information on its own.
Schmidt Decomposition
Any two-qubit pure state can be written as:
|ψ⟩ = Σ λᵢ |uᵢ⟩ ⊗ |vᵢ⟩
where λᵢ ≥ 0 are Schmidt coefficients. If only one λᵢ is nonzero, the state is a product state. If multiple are nonzero, the state is entangled.
def schmidt_coefficients(psi_2q):
"""Compute Schmidt coefficients of a 2-qubit state."""
matrix = psi_2q.reshape(2, 2)
_, singular_values, _ = np.linalg.svd(matrix)
return singular_values
# Product state |+0⟩
product = np.kron(plus, ket_0)
print("Product state Schmidt:", schmidt_coefficients(product))
# [1.0, 0.0] -- only one nonzero coefficient = product state (separable)
# |+⟩⊗|0⟩ = [0.707, 0, 0.707, 0], reshaped to [[0.707, 0], [0.707, 0]]
# which is a rank-1 matrix, so SVD gives singular values [1.0, 0.0]
print(np.linalg.matrix_rank(product.reshape(2,2))) # 1 = product state
# Bell state
print("Bell state Schmidt:", schmidt_coefficients(bell))
# [0.707, 0.707] -- two equal nonzero values = maximally entangled
Summary: Key Objects and Their Roles
| Object | Notation | Math | Role |
|---|---|---|---|
| Pure state | ψ⟩ | Unit complex vector | |
| Bra | ⟨ψ | Conjugate transpose | |
| Inner product | ⟨φ | ψ⟩ | φ† · ψ |
| Outer product | ψ⟩⟨φ | ||
| Density matrix | ρ | ψ⟩⟨ψ | |
| Tensor product | ψ⟩⊗ | φ⟩ | |
| Unitary matrix | U | U†U = I | Quantum gate |
| Hermitian matrix | H | H† = H | Observable |
| Eigenvalue | λ | Hv = λv | Measurement outcome |
| Partial trace | Tr_B(ρ_AB) | Traced density matrix | Reduced state |
These are the building blocks of every quantum algorithm, every quantum error correction scheme, and every quantum information proof. With a solid grip on these objects in numpy, you can read and implement quantum computing research directly.
Was this tutorial helpful?