|
| 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) |
0 commit comments