Skip to content

Commit 5f3acbf

Browse files
committed
update
1 parent cfb6a99 commit 5f3acbf

89 files changed

Lines changed: 26635 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
1.77 MB
Binary file not shown.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Programs for chapter 1
2+
3+
Every number, table and figure of chapter 1 is produced by the programs in this folder (see `../../CLAUDE.md` and the `qc-book-code` skill). Keep this list and the `Programs for this chapter` section of `chapter1.tex` in sync.
4+
5+
| Program | What it computes | Runtime |
6+
|---|---|---|
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Programs for chapter 10
2+
3+
Every number, table and figure of chapter 10 is produced by the programs in this folder (see `../../CLAUDE.md` and the `qc-book-code` skill). Keep this list and the `Programs for this chapter` section of `chapter10.tex` in sync.
4+
5+
| Program | What it computes | Runtime |
6+
|---|---|---|
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Programs for chapter 11
2+
3+
Every number, table and figure of chapter 11 is produced by the programs in this folder (see `../../CLAUDE.md` and the `qc-book-code` skill). Keep this list and the `Programs for this chapter` section of `chapter11.tex` in sync.
4+
5+
| Program | What it computes | Runtime |
6+
|---|---|---|
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Programs for chapter 2
2+
3+
Every number, table and figure of chapter 2 is produced by the programs in this folder together with the shared modules in `../common/` that this chapter introduces (see `../../CLAUDE.md` and the `qc-book-code` skill). Keep this list and the `Programs for this chapter` section of `chapter2.tex` in sync. Figures are written to `../../BookFigures/chapter2/` as PDF (for LaTeX) and PNG (for the notebook).
4+
5+
| Program | What it computes | Runtime |
6+
|---|---|---|
7+
| `dirac_algebra.py` | Worked examples of Sec. 2.2: inner/outer products, projectors, Pauli algebra, tensor products, spectral decomposition | instant |
8+
| `bloch_sphere.py` | Bloch angles and vectors, Table 2.1, Fig. 2.1 (Pauli eigenstates, Larmor precession, rotations) | seconds |
9+
| `measurement.py` | Born rule, post-measurement states, X-basis measurement, marginals of a 3-qubit register, Fig. 2.2 (shot noise) | seconds |
10+
| `decoherence.py` | Dephasing and amplitude-damping channels (Kraus form), Fig. 2.3 | instant |
11+
| `entanglement.py` | Bell states, measurement correlations, partial traces, Schmidt decomposition via SVD, Table 2.2, Fig. 2.4 | instant |
12+
| `protocols.py` | Superdense coding (all four messages) and teleportation (200 random states) on the simulator | instant |
13+
| `two_level_system.py` | Two-level Hamiltonian in matrix and Pauli form, Table 2.3, grid search over the R_y R_x ansatz, Fig. 2.8 | seconds |
14+
15+
Shared modules introduced by this chapter (in `../common/`): `gates.py`, `statevector.py`, `circuit.py`, `density_matrix.py`, `bookplot.py`. All programs run on `numpy` and `matplotlib` only; each runs as a script with `assert`-based checks.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Chapter 2, Sec. 2.3: the Bloch sphere.
2+
3+
Converts between the amplitudes of a qubit and its Bloch angles
4+
(Eq. 2.blochstate), computes the Bloch vector r = (<X>, <Y>, <Z>)
5+
(Eq. 2.blochvector), lists the Bloch angles of the six Pauli eigenstates
6+
(Table 2.paulieigen) and draws Fig. 2.bloch: the sphere with the Pauli
7+
eigenstates, the trajectory of a spin precessing in a magnetic field along
8+
z (Eq. 2.larmor) and the two-rotation path R_y(phi) R_x(theta)|0> used as a
9+
variational ansatz in Sec. 2.11. Runs in a few seconds.
10+
"""
11+
import sys, pathlib
12+
root = pathlib.Path(__file__).resolve().parents[1]
13+
sys.path[:0] = [str(p) for p in root.iterdir() if p.is_dir()]
14+
15+
import numpy as np
16+
import gates as G
17+
from bookplot import savefig, plt
18+
19+
20+
def bloch_state(theta, phi):
21+
"""|psi> = cos(theta/2)|0> + e^{i phi} sin(theta/2)|1>."""
22+
return np.array([np.cos(theta / 2), np.exp(1j * phi) * np.sin(theta / 2)])
23+
24+
25+
def bloch_angles(psi):
26+
"""Inverse of bloch_state, removing the global phase first."""
27+
psi = np.asarray(psi, dtype=complex)
28+
psi = psi / np.linalg.norm(psi)
29+
psi = psi * np.exp(-1j * np.angle(psi[0])) # make the |0> amplitude real
30+
theta = 2 * np.arccos(np.clip(psi[0].real, -1, 1))
31+
phi = np.angle(psi[1]) if abs(psi[1]) > 1e-12 else 0.0
32+
return theta, phi % (2 * np.pi)
33+
34+
35+
def bloch_vector(psi):
36+
"""r = (<X>, <Y>, <Z>) for a pure single-qubit state."""
37+
psi = np.asarray(psi, dtype=complex)
38+
return np.array([np.vdot(psi, P @ psi).real for P in (G.X, G.Y, G.Z)])
39+
40+
41+
PAULI_EIGENSTATES = {
42+
"|0>": ([1, 0], "Z", +1), "|1>": ([0, 1], "Z", -1),
43+
"|+>": ([1, 1] / np.sqrt(2), "X", +1), "|->": ([1, -1] / np.sqrt(2), "X", -1),
44+
"|+i>": ([1, 1j] / np.sqrt(2), "Y", +1), "|-i>": ([1, -1j] / np.sqrt(2), "Y", -1),
45+
}
46+
47+
48+
def larmor_trajectory(theta0, phi0, omega, times):
49+
"""Points on the sphere for |psi(t)> = exp(-i H t) |psi(0)>, H = -omega Z/2."""
50+
psi0 = bloch_state(theta0, phi0)
51+
pts = []
52+
for t in times:
53+
U = G.Rz(-omega * t) # exp(-i H t) = exp(+i omega t Z / 2)
54+
pts.append(bloch_vector(U @ psi0))
55+
return np.array(pts)
56+
57+
58+
def draw_bloch(ax, elev=20, azim=35):
59+
u, v = np.mgrid[0:2 * np.pi:60j, 0:np.pi:30j]
60+
ax.plot_surface(np.cos(u) * np.sin(v), np.sin(u) * np.sin(v), np.cos(v),
61+
color="lightsteelblue", alpha=0.15, linewidth=0)
62+
t = np.linspace(0, 2 * np.pi, 200)
63+
for circ in ((np.cos(t), np.sin(t), 0 * t), (np.cos(t), 0 * t, np.sin(t)), (0 * t, np.cos(t), np.sin(t))):
64+
ax.plot(*circ, color="gray", lw=0.5)
65+
for axis, label in (((1.3, 0, 0), "x"), ((0, 1.3, 0), "y"), ((0, 0, 1.3), "z")):
66+
ax.quiver(0, 0, 0, *axis, color="k", arrow_length_ratio=0.08, lw=0.8)
67+
ax.text(*(1.4 * np.array(axis)), label)
68+
ax.set_box_aspect((1, 1, 1)); ax.set_axis_off(); ax.view_init(elev=elev, azim=azim)
69+
ax.set_xlim(-1.2, 1.2); ax.set_ylim(-1.2, 1.2); ax.set_zlim(-1.2, 1.2)
70+
71+
72+
if __name__ == "__main__":
73+
np.set_printoptions(precision=4, suppress=True)
74+
# Table 2.paulieigen
75+
print(f"{'state':6s} {'operator':9s} {'eigenvalue':>10s} {'theta':>8s} {'phi':>8s} Bloch vector")
76+
for name, (psi, op, ev) in PAULI_EIGENSTATES.items():
77+
th, ph = bloch_angles(psi)
78+
print(f"{name:6s} {op:9s} {ev:>10d} {th/np.pi:7.2f}π {ph/np.pi:7.2f}π {bloch_vector(psi)}")
79+
# round trip amplitudes -> angles -> amplitudes
80+
theta, phi = 2.0, 0.7
81+
psi = bloch_state(theta, phi)
82+
assert np.allclose(bloch_angles(psi), (theta, phi))
83+
assert np.allclose(bloch_vector(psi), [np.sin(theta) * np.cos(phi), np.sin(theta) * np.sin(phi), np.cos(theta)])
84+
# a global phase leaves the Bloch vector unchanged
85+
assert np.allclose(bloch_vector(np.exp(0.3j) * psi), bloch_vector(psi))
86+
print("\nBloch angles of psi =", psi, "->", f"theta={theta}, phi={phi}")
87+
88+
# Figure 2.bloch
89+
fig = plt.figure(figsize=(9, 4.4))
90+
ax = fig.add_subplot(1, 2, 1, projection="3d")
91+
draw_bloch(ax)
92+
for name, (v, op, ev) in PAULI_EIGENSTATES.items():
93+
r = bloch_vector(v)
94+
ax.scatter(*r, color="C3", s=25)
95+
ax.text(*(1.12 * r), name, fontsize=8)
96+
r = bloch_vector(psi)
97+
ax.quiver(0, 0, 0, *r, color="C0", arrow_length_ratio=0.1, lw=2)
98+
ax.text(*(1.1 * r), r"$|\psi\rangle$", color="C0")
99+
ax.set_title("(a) Pauli eigenstates and a general state", fontsize=9)
100+
101+
ax = fig.add_subplot(1, 2, 2, projection="3d")
102+
draw_bloch(ax)
103+
traj = larmor_trajectory(theta0=1.0, phi0=0.0, omega=1.0, times=np.linspace(0, 2 * np.pi, 200))
104+
ax.plot(traj[:, 0], traj[:, 1], traj[:, 2], color="C1", lw=2, label="Larmor precession")
105+
# the path R_y(phi) R_x(theta)|0>: first rotate about x, then about y
106+
k0 = np.array([1, 0], dtype=complex)
107+
th_path = np.linspace(0, 2.0, 60)
108+
p1 = np.array([bloch_vector(G.Rx(t) @ k0) for t in th_path])
109+
ph_path = np.linspace(0, 1.2, 60)
110+
p2 = np.array([bloch_vector(G.Ry(p) @ G.Rx(2.0) @ k0) for p in ph_path])
111+
ax.plot(p1[:, 0], p1[:, 1], p1[:, 2], color="C2", lw=2, label=r"$R_x(\theta)|0\rangle$")
112+
ax.plot(p2[:, 0], p2[:, 1], p2[:, 2], color="C4", lw=2, label=r"$R_y(\phi)R_x(2)|0\rangle$")
113+
ax.legend(loc="lower left", fontsize=7)
114+
ax.set_title("(b) rotations and precession", fontsize=9)
115+
out = savefig(fig, 2, "bloch_sphere")
116+
print("figure written to", out)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Chapter 2, Sec. 2.5.5: decoherence seen through the density matrix.
2+
3+
Two elementary noise channels on one qubit, written as Kraus maps
4+
rho -> sum_k E_k rho E_k^dagger (Eq. 2.kraus): dephasing, which shrinks the
5+
off-diagonal elements of rho by a factor (1 - 2p) and leaves the populations
6+
untouched (Eq. 2.dephasing), and amplitude damping, which drives the qubit
7+
towards |0> (Eq. 2.damping). Fig. 2.decoherence shows the Bloch vector of
8+
|+> under repeated application of each channel and the accompanying loss of
9+
purity. Runs in a second.
10+
"""
11+
import sys, pathlib
12+
root = pathlib.Path(__file__).resolve().parents[1]
13+
sys.path[:0] = [str(p) for p in root.iterdir() if p.is_dir()]
14+
15+
import numpy as np
16+
import gates as G
17+
from density_matrix import DensityMatrix
18+
from bookplot import savefig, plt
19+
20+
21+
def kraus_apply(rho, kraus):
22+
"""rho -> sum_k E_k rho E_k^dagger."""
23+
return sum(E @ rho @ E.conj().T for E in kraus)
24+
25+
26+
def dephasing(p):
27+
"""With probability p a Z error hits the qubit."""
28+
return [np.sqrt(1 - p) * G.I, np.sqrt(p) * G.Z]
29+
30+
31+
def amplitude_damping(gamma):
32+
"""|1> decays to |0> with probability gamma (spontaneous emission)."""
33+
E0 = np.array([[1, 0], [0, np.sqrt(1 - gamma)]], dtype=complex)
34+
E1 = np.array([[0, np.sqrt(gamma)], [0, 0]], dtype=complex)
35+
return [E0, E1]
36+
37+
38+
def trajectory(rho0, kraus, steps):
39+
rho = rho0.copy()
40+
out = [DensityMatrix(rho)]
41+
for _ in range(steps):
42+
rho = kraus_apply(rho, kraus)
43+
out.append(DensityMatrix(rho))
44+
return out
45+
46+
47+
if __name__ == "__main__":
48+
np.set_printoptions(precision=4, suppress=True)
49+
plus = np.array([1, 1]) / np.sqrt(2)
50+
rho0 = np.outer(plus, plus.conj())
51+
# Kraus operators satisfy sum_k E_k^dagger E_k = I
52+
for kraus in (dephasing(0.1), amplitude_damping(0.1)):
53+
assert np.allclose(sum(E.conj().T @ E for E in kraus), G.I)
54+
# one dephasing step: off-diagonals scaled by 1 - 2p, Eq. (2.dephasing)
55+
p = 0.1
56+
rho1 = kraus_apply(rho0, dephasing(p))
57+
assert np.allclose(rho1, [[0.5, 0.5 * (1 - 2 * p)], [0.5 * (1 - 2 * p), 0.5]])
58+
print("rho(|+>) after one dephasing step with p = 0.1:\n", rho1.real)
59+
steps = 30
60+
deph = trajectory(rho0, dephasing(0.05), steps)
61+
damp = trajectory(rho0, amplitude_damping(0.08), steps)
62+
print(f"{'step':>4s} {'purity (dephasing)':>20s} {'purity (damping)':>18s} {'<Z> (damping)':>14s}")
63+
for k in (0, 5, 10, 20, 30):
64+
print(f"{k:4d} {deph[k].purity():20.4f} {damp[k].purity():18.4f} {damp[k].bloch_vector()[2]:14.4f}")
65+
# the fully dephased state is the maximally mixed state, entropy 1 bit
66+
final = trajectory(rho0, dephasing(0.5), 1)[-1]
67+
assert np.allclose(final.rho, np.eye(2) / 2) and abs(final.von_neumann_entropy() - 1) < 1e-12
68+
69+
fig, axes = plt.subplots(1, 2, figsize=(8, 3.2))
70+
t = np.arange(steps + 1)
71+
for ax, traj, title in ((axes[0], deph, "dephasing, $p=0.05$ per step"),
72+
(axes[1], damp, r"amplitude damping, $\gamma=0.08$ per step")):
73+
r = np.array([d.bloch_vector() for d in traj])
74+
ax.plot(t, r[:, 0], label=r"$\langle X\rangle$")
75+
ax.plot(t, r[:, 2], label=r"$\langle Z\rangle$")
76+
ax.plot(t, [d.purity() for d in traj], "k--", label=r"purity $\mathrm{Tr}\rho^2$")
77+
ax.plot(t, [d.von_neumann_entropy() for d in traj], "k:", label=r"entropy $S(\rho)$")
78+
ax.set_xlabel("channel applications"); ax.set_title(title, fontsize=9)
79+
axes[0].set_ylim(-0.05, 1.05); axes[1].set_ylim(-0.05, 1.05)
80+
axes[0].legend(fontsize=8, loc="center right")
81+
print("figure written to", savefig(fig, 2, "decoherence"))
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Chapter 2, Sec. 2.2: the worked linear-algebra examples of the text.
2+
3+
Inner and outer products in Dirac notation (Eqs. 2.innerex-2.outer),
4+
projection operators and their action on a superposition (Eq. 2.project),
5+
Pauli algebra (Eqs. 2.pauliprod-2.paulicomm), tensor products of states and
6+
matrices (Eqs. 2.tensorstate-2.tensormatrix) and the spectral decomposition
7+
of a Hermitian 2x2 matrix (Eq. 2.spectral). Runs in well under a second.
8+
"""
9+
import sys, pathlib
10+
root = pathlib.Path(__file__).resolve().parents[1] # BookPrograms/
11+
sys.path[:0] = [str(p) for p in root.iterdir() if p.is_dir()]
12+
13+
import numpy as np
14+
import gates as G
15+
16+
17+
def ket(*amplitudes):
18+
return np.array(amplitudes, dtype=complex)
19+
20+
21+
def bra(v):
22+
return v.conj()
23+
24+
25+
def inner(u, v):
26+
"""<u|v> = sum_i u_i^* v_i."""
27+
return np.vdot(u, v)
28+
29+
30+
def outer(u, v):
31+
"""|u><v|, the matrix u_i v_j^*."""
32+
return np.outer(u, v.conj())
33+
34+
35+
def spectral_decomposition(A):
36+
"""Eigenvalues lambda_i and projectors P_i = |i><i| with A = sum_i lambda_i P_i."""
37+
lam, V = np.linalg.eigh(A)
38+
projectors = [outer(V[:, i], V[:, i]) for i in range(len(lam))]
39+
return lam, projectors
40+
41+
42+
if __name__ == "__main__":
43+
np.set_printoptions(precision=4, suppress=True)
44+
# --- Eq. (2.innerex): a two-component complex vector and its norm
45+
x = ket(1 - 1j, 2 + 1j)
46+
print("<x|x> =", inner(x, x).real, " normalised x =", x / np.sqrt(inner(x, x).real))
47+
# --- Eq. (2.innerasym): <x|y>^* = <y|x>
48+
x = ket(-1, 2j, 1); y = ket(1, 0, 1j)
49+
print("<x|y> =", inner(x, y), " <y|x> =", inner(y, x))
50+
assert inner(x, y) == np.conj(inner(y, x))
51+
# --- computational basis, identity as a sum of projectors, Eq. (2.completeness)
52+
k0, k1 = ket(1, 0), ket(0, 1)
53+
P, Q = outer(k0, k0), outer(k1, k1)
54+
assert np.allclose(P + Q, np.eye(2)) and np.allclose(P @ P, P) and np.allclose(P @ Q, 0)
55+
print("P = |0><0| =\n", P.real, "\nQ = |1><1| =\n", Q.real)
56+
# --- projecting a superposition, Eq. (2.project)
57+
alpha, beta = 0.6, 0.8j
58+
psi = alpha * k0 + beta * k1
59+
print("P|psi> =", P @ psi, " Q|psi> =", Q @ psi)
60+
# --- the density matrix of the superposition, Eq. (2.rhopure2)
61+
rho = outer(psi, psi)
62+
print("rho = |psi><psi| =\n", rho, "\ntrace =", np.trace(rho).real)
63+
# --- Pauli algebra, Eqs. (2.pauliprod)-(2.paulicomm)
64+
for a, b, c in (("X", "Y", "Z"), ("Y", "Z", "X"), ("Z", "X", "Y")):
65+
assert np.allclose(G.commutator(G.PAULIS[a], G.PAULIS[b]), 2j * G.PAULIS[c])
66+
assert np.allclose(G.PAULIS[a] @ G.PAULIS[b], 1j * G.PAULIS[c])
67+
print("[X,Y] = 2iZ, XY = iZ, and cyclic permutations: verified")
68+
# --- Hadamard on the basis states, Eq. (2.hadamardaction)
69+
print("H|0> =", G.H @ k0, " H|1> =", G.H @ k1)
70+
# --- tensor products, Eqs. (2.tensorstate)-(2.tensormatrix)
71+
for a, ka in ((0, k0), (1, k1)):
72+
for b, kb in ((0, k0), (1, k1)):
73+
print(f"|{a}{b}> =", np.kron(ka, kb).real)
74+
print("|000> =", np.kron(np.kron(k0, k0), k0).real)
75+
A = np.array([[1, 2], [3, 4]]); B = np.array([[0, 1], [1, 0]])
76+
print("A (x) B =\n", np.kron(A, B))
77+
# --- spectral decomposition of a Hermitian matrix, Eq. (2.spectral)
78+
Aherm = np.array([[2, 1 - 1j], [1 + 1j, 3]])
79+
lam, projs = spectral_decomposition(Aherm)
80+
rebuilt = sum(l * Pj for l, Pj in zip(lam, projs))
81+
assert np.allclose(rebuilt, Aherm)
82+
print("eigenvalues of A:", lam)
83+
for l, Pj in zip(lam, projs):
84+
print(f"lambda = {l:.4f}, projector\n", Pj)
85+
# a function of the operator through its spectral decomposition
86+
expA = sum(np.exp(l) * Pj for l, Pj in zip(lam, projs))
87+
from math import factorial
88+
series = sum(np.linalg.matrix_power(Aherm, k) / factorial(k) for k in range(40))
89+
assert np.allclose(expA, series)
90+
print("exp(A) from the spectral decomposition agrees with the power series")

0 commit comments

Comments
 (0)