5. Tensor Products
5.1Why ordinary vectors aren't enough
One qubit is a vector in ℂ². But two qubits are not a vector in ℂ² "somehow doubled" — they need four amplitudes (00, 01, 10, 11), three need eight, and n need 2ⁿ. No ordinary vector operation on the individual state vectors produces this: adding or concatenating would give 2 or 4 components, not 2². The mathematics needs one new operation whose dimension count multiplies, because physical composition multiplies outcomes: two coins have four joint outcomes, not two. The tensor product is that operation. Everything counterintuitive about quantum scale follows from it — the exponential state space, entanglement, and the impossibility of classical simulation (7.12) — and everything practical about multi-qubit software depends on getting its conventions right. This chapter is the point where the book's subject becomes genuinely many-body; read it slowly and run every snippet.
5.2Composite systems
When two physical systems are considered jointly, the joint state space is the tensor product of the individual spaces: ℂ² ⊗ ℂ² ≅ ℂ⁴, and in general dim(A ⊗ B) = dim(A)·dim(B). The rule encodes a physical principle: joint outcomes are pairs (a, b), and there are dim(A)·dim(B) such pairs. For qubits: n qubits live in (ℂ²)^{⊗n} = ℂ^{2ⁿ} — the exponential that defines the field. Crucially, the composite space contains strictly more than the pairs of individual states: most vectors in ℂ⁴ are not expressible as (single state of qubit 1) ⊗ (single state of qubit 2). Those extra, un-factorizable vectors are entangled states (5.8) — correlations with no classical counterpart, the resource behind teleportation, superdense coding, and error-correcting codes. Composition is multiplication in dimensions and something genuinely new in content.
5.3Tensor-product notation
The tensor product of vectors is written |a⟩ ⊗ |b⟩, usually abbreviated |a⟩|b⟩ or |ab⟩: for |a⟩ = (a₀, a₁) and |b⟩ = (b₀, b₁), the product is the four-component vector (a₀b₀, a₀b₁, a₁b₀, a₁b₁). The rule generalizes componentwise: every component of the first multiplies every component of the second. Properties that matter: the tensor product is bilinear — scalars pull out, and (a₁ + a₂) ⊗ b = a₁ ⊗ b + a₂ ⊗ b — but not commutative; |a⟩ ⊗ |b⟩ and |b⟩ ⊗ |a⟩ live in differently-labeled spaces (qubit 1 versus qubit 2), and conflating them is a bug. For basis states the notation doubles as labels: |0⟩ ⊗ |1⟩ = |01⟩, the two-qubit basis state reading "first qubit 0, second qubit 1". From here on, n-qubit states are written |b₁b₂…bₙ⟩ with each bᵢ a bit.
5.4Kronecker products
The Kronecker product is the tensor product's implementation for matrices: A ⊗ B is the block matrix with block (i, j) equal to aᵢⱼ·B — every entry of A scaled into a copy of B. numpy ships it as np.kron(A, B). The algebra you need: (A ⊗ B)(C ⊗ D) = (AC) ⊗ (BD) — mixed products combine pairwise, the identity that lets circuits of local gates be compiled into one big matrix; (A ⊗ B)† = A† ⊗ B†; and (A ⊗ B)(v ⊗ w) = (Av) ⊗ (Bw). Dimensions multiply: (m×n) ⊗ (p×q) is (mp×nq). One sharp edge: Kronecker is not commutative — np.kron(A, B) and np.kron(B, A) are different matrices related by a permutation of basis order (5.10). Every multi-qubit simulator you will ever read is, underneath, a disciplined bookkeeping system for np.kron calls.
import numpy as np
I2 = np.eye(2, dtype=complex)
X = np.array([[0, 1], [1, 0]], dtype=complex)5.5Tensor-product dimensions
Dimensions multiply: (ℂ^{d₁}) ⊗ (ℂ^{d₂}) has dimension d₁·d₂, so n qubits give 2ⁿ amplitudes, n qutrits give 3ⁿ, and a hybrid system gives the product of its parts' dimensions. This multiplicative law is the engine of both the promise and the pain. Promise: 300 qubits span 2³⁰⁰ amplitudes — room to encode structures no classical memory can hold. Pain: every simulation object — state vector, gate, density matrix — scales with the product, so 30 qubits at 16 bytes per complex128 is 16 GB for one state vector and 2⁶⁰ entries for a dense operator (which is why even simulators avoid forming full multi-qubit operators, applying them factor-wise instead, 7.11). Dimension arithmetic is also a debugging tool: after building any composite object, assert its shape. assert psi.shape == (2**n,) catches the off-by-one-qubit errors that would otherwise surface as inexplicable statistics, much later.
5.6Multi-qubit states
An n-qubit state is a unit vector in ℂ^{2ⁿ}: ψ = Σ_x c_x |x⟩, where x runs over all n-bit strings and Σ_x |c_x|² = 1. Each c_x is the amplitude of the classical bit pattern x; measuring in the computational basis yields pattern x with probability |c_x|². There is no separate "state of each qubit" in general — the joint vector is the full description, and individual-qubit descriptions exist only when the state is separable (5.7). Read concrete examples until they are reflexes: |00⟩ is the vector (1,0,0,0); (|00⟩ + |11⟩)/√2 has two nonzero amplitudes and is the Bell state, the canonical entangled pair; the state (|00⟩ + |01⟩ + |10⟩ + |11⟩)/2 is two independent qubits each in |+⟩. The gap between four amplitudes and "two qubits each having a state" is exactly the content of the next two sections.
5.7Separable states
A composite state is separable (a product state) when it can be written ψ = φ ⊗ ω — one vector for each subsystem. For such states, everything factors: probabilities of joint outcomes multiply, each subsystem has its own well-defined state vector, and the whole is no more than the parts. These are the quantum analogues of independent random variables, and they are what you get from preparing each qubit separately: np.kron(psi1, psi2). Product states are also precisely the states classical simulation handles comfortably — memory for two n₁- and n₂-qubit factors is 2^{n₁} + 2^{n₂}, not 2^{n₁+n₂}. That observation is not a trick but a research program: represent states as networks of small factors wherever physics permits (5.14), and the exponential wall recedes as far as the correlations stay shallow. Sep-arability is therefore not a footnote — it is the boundary of the efficiently simulable world.
5.8Non-separable states
An entangled state is a composite state that cannot be factored: ψ ≠ φ ⊗ ω for any choice of factors. The canonical case is the Bell state (|00⟩ + |11⟩)/√2: neither qubit has a state vector of its own, yet joint measurements are perfectly correlated — outcomes always agree, at distance, with no shared classical cause available in time. Entanglement is quantifiable (entropy of entanglement, negativity — chapter 34), generable by gates as ordinary as CNOT acting on |+0⟩, and consumed as a resource by teleportation, superdense coding, and error-correcting codes. For simulation, entanglement is the expense: Bell-type states across many qubits resist every compressed representation. The honest engineering summary: separability is cheap, entanglement is powerful, and the amount of entanglement your problem genuinely needs determines whether you can study it on a laptop or must queue for hardware.
import numpy as np
plus = np.array([1, 1], dtype=complex) / np.sqrt(2)
ket0 = np.array([1, 0], dtype=complex)
psi = np.kron(plus, ket0) # |+0>: separable
M = psi.reshape(2, 2) # view as matrix
print("rank of coefficients:", np.linalg.matrix_rank(M)) # 1 => product state
bell = (np.kron(ket0, ket0) + np.kron(ket0, np.array([0, 1], dtype=complex))) / np.sqrt(2)
print("Bell rank:", np.linalg.matrix_rank(bell.reshape(2, 2))) # 2 => entangled5.9Computational basis states
The computational basis of n qubits is the set {|x⟩ : x an n-bit string}, built by tensoring single-qubit basis states: |b₁b₂…bₙ⟩ = |b₁⟩ ⊗ |b₂⟩ ⊗ … ⊗ |bₙ⟩. There are 2ⁿ of them, mutually orthonormal, and they are the classical skeleton of the quantum space — the outcomes measurements return, the labels memory addresses would carry. Each basis state is a one-hot vector: |x⟩ has a single 1 among 2ⁿ entries, so as data it is maximally sparse, and simulators exploit this by representing many algorithms' starting states implicitly (index x means amplitude 1 at position x) rather than materializing the array. Every state is a linear combination of basis states with amplitudes c_x = ⟨x|ψ⟩. When a later chapter says "the state is peaked on basis state x", it means exactly one amplitude dominates — the interference choreography of all previous chapters, stated as a sentence about indices.
5.10Basis ordering conventions
The same four numbers (c₀₀, c₀₁, c₁₀, c₁₁) can mean two different states, depending on which bit is "first". The dominant convention — big-endian in mathematics, and the source of endless Qiskit confusion — labels |q₁ q₂⟩ with q₁ the leftmost. numpy's np.kron(A, B) applies A to the leftmost (most significant) factor. Qiskit historically labels qubits little-endian: qubit 0 rightmost, so kron(q1, q0)-style ordering appears in statevector dumps. Neither is wrong; mixing them silently corrupts everything — CNOT control and target swap roles, statistics come out mirrored. Engineering defenses, all cheap: document the convention in one module-level comment; write a self-test that prepares a known asymmetric state like |01⟩ and asserts where its 1 sits (np.argmax(psi) == index); prefer round-trip tests over eyeballing. Convention bugs produce plausible, wrong physics — the worst bug class in this field.
5.11Tensor-product operators
Operators on composite systems tensor the same way: (A ⊗ B)|a⟩|b⟩ = (A|a⟩) ⊗ (B|b⟩). The workhorse pattern is A applied to one qubit of many: I ⊗ I ⊗ A ⊗ I ⊗ …, padded with identities on untouched factors — np.kron chains, or in production code, application without materialization (7.11). Pauli tensor products deserve special status: any operator on n qubits expands in the Pauli basis {I, X, Y, Z}^{⊗n}, so Hamiltonians ship as weighted sums like 0.5·X⊗Z + 1.2·Z⊗I — the file format of quantum chemistry and of every Trotter-step simulator (chapter 30). The mixed-product identity (A⊗B)(C⊗D) = AC⊗BD lets whole circuits compile into single matrices when you can afford the exponential size — and reminds you why you usually cannot: two-qubit gates alone make a 2ⁿ×2ⁿ object no simulator should form explicitly beyond a dozen qubits.
5.12Partial operations
A partial operation acts on some qubits and leaves others alone: to apply single-qubit gate G to qubit k of n, build G_k = I^{⊗(n−1−k)} ⊗ G ⊗ I^{⊗k} (in big-endian ordering) and multiply. For two-qubit gates like CNOT on non-adjacent qubits, insert SWAPs or index directly. This is the everyday construction of every circuit simulator, and it is where the conventions of 5.10 become code. Beyond gates, partial measurement works the same way: to measure qubit k in the computational basis, project with P₀^{(k)} = I ⊗ P₀ ⊗ I and P₁^{(k)}, with probabilities ⟨ψ|Pᵢ^{(k)}|ψ⟩, and renormalize the surviving branch. Two-qubit CNOT from primitives is worth deriving once by hand: CNOT = P₀ ⊗ I + P₁ ⊗ X — "if control is 0, do nothing; if 1, apply X" — the cleanest possible demonstration that projectors and tensor products already contain conditional logic.
5.13Partial traces
The partial trace reverses composition in the only way quantum mechanics allows: it answers "what does subsystem A look like, ignoring B?" without pretending B has a definite state. Given a density matrix ρ on AB, the reduced state is ρ_A = Tr_B(ρ), computed blockwise: for ρ on A⊗B with B of dimension d, ρ_A[(i, i′), (j, j′)] sums ρ over the diagonal blocks of B: ρ_A[i·d + j, i′·d + j′] summed over j = j′. In numpy, reshape ρ to (d_A, d_B, d_A, d_B) and trace over the two B axes: rho_A = np.einsum('ijkj->ik', rho.reshape(dA, dB, dA, dB)). The result is the exact object needed when a subsystem is discarded or unobserved — the density-matrix formalism of chapter 33 exists because partial traces of pure states are generally mixed. For the Bell state, ρ_A is ½I: each qubit alone is maximally random; all the structure lives in the correlations.
5.14Tensor networks — first encounter
The full state vector stores 2ⁿ amplitudes; a tensor network stores instead a mesh of small tensors connected by contracted indices, with size governed by a bond dimension χ that tracks how much entanglement crosses each cut. Product states are χ = 1 networks; matrix product states (MPS), the one-dimensional case, capture ground states of many physical systems with modest χ. This is the idea behind the classical simulators that contested quantum-supremacy claims: Google's 2019 Sycamore circuit was simulated classically by tensor-network methods at costs far below the naive exponential, and the game of "network versus state vector" continues with every new hardware announcement. The trade is precise: networks shine when entanglement is low or structured, and degrade toward the full exponential as χ grows. Chapter 45 develops the algorithms; here, hold the design lesson — representations that match the physics's correlation structure beat generic ones.