Part 1: Theoretical Background¶
Pure States and the Bloch Sphere¶
Every pure qubit state (after the global phase is removed) can be written as
The corresponding Bloch vector is defined as the expectation values of the three Pauli operators:
Detailed Computation: Deriving ¶
Denote the components of by (a real number) and , that is,
Step 1. Compute
Substituting and :
Step 2. Compute
Substituting and :
Step 3. Compute
Substituting as above:
Step 4. Summary of the results
This vector satisfies ; that is, lies on the unit sphere.
Computing the Uncertainties, and a Conserved Quantity¶
Since , we have for every state, and therefore
Geometric meaning: equals the perpendicular distance from the Bloch point to the -th coordinate axis.
Derivation of the conserved quantity.
This is a purely geometric fact: for a point on the surface of the Bloch sphere, the sum of the squared distances to the three coordinate axes always equals 2, regardless of the choice of . Quantum gates (unitary transformations) move the Bloch point on the sphere but do not change this conserved quantity.
The Geometric Meaning of Quantum Gates (Connection with §9.2.3)¶
| Quantum gate | Matrix | Geometric action on the Bloch sphere |
|---|---|---|
| H (Hadamard) | Rotation by 180° about the bisector of the - and -axes; swaps | |
| S (phase gate) | Rotation by 90° about the -axis | |
| T ( gate) | Rotation by 45° about the -axis | |
| X (Pauli-X) | Rotation by 180° about the -axis (bit flip) | |
| Y (Pauli-Y) | Rotation by 180° about the -axis | |
| Z (Pauli-Z) | Rotation by 180° about the -axis (phase flip) |
Applying a 2×2 unitary matrix to a state () is equivalent to applying an SO(3) rotation to the Bloch vector : it does not change the radius of the sphere (a pure state remains a pure state), but it changes .
The Controls¶
# Colab: enable the custom widget manager so that FigureWidget and the sliders are interactive; skipped elsewhere
try:
from google.colab import output
output.enable_custom_widget_manager()
except ImportError:
pass
import plotly.graph_objects as go
import numpy as np
from ipywidgets import widgets
# ── Color scheme (Accent Mix) ──────────────────────
C_BG = "#F8F8F8"
C_GRID = "#D6D6D6"
C_V1 = "#57068C" # Bloch vector (violet)
C_V2 = "#006385" # y-axis (deep blue)
C_T1 = "#2AD2C9" # z-axis (teal)
C_WARN = "#FF5D47" # x-axis (orange)
C_AUX = "#AB82C5" # vector after a quantum gate (light violet)
C_AREA = "rgba(87,6,140,0.15)"
# ── Quantum gate definitions ──────────────────────────────────
GATES = {
"H": np.array([[1, 1],[1,-1]], dtype=complex) / np.sqrt(2),
"S": np.array([[1, 0],[0, 1j]], dtype=complex),
"T": np.array([[1, 0],[0, np.exp(1j*np.pi/4)]], dtype=complex),
"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),
}
# ── Pure state ↔ Bloch vector conversion ─────────────────────
def angles_to_ket(theta_deg, phi_deg):
'''(theta deg, phi deg) -> normalized ket'''
th = np.radians(theta_deg)
ph = np.radians(phi_deg)
return np.array([np.cos(th/2), np.exp(1j*ph)*np.sin(th/2)], dtype=complex)
def ket_to_bloch(psi):
'''normalized ket -> Bloch vector (ex, ey, ez)'''
sx = np.array([[0,1],[1,0]], dtype=complex)
sy = np.array([[0,-1j],[1j,0]], dtype=complex)
sz = np.array([[1,0],[0,-1]], dtype=complex)
ex = float(np.real(psi.conj() @ sx @ psi))
ey = float(np.real(psi.conj() @ sy @ psi))
ez = float(np.real(psi.conj() @ sz @ psi))
return ex, ey, ez
def ket_to_angles(psi):
'''normalized ket -> (theta_deg, phi_deg), global phase normalized'''
psi = psi / np.linalg.norm(psi)
# make component 0, psi[0], real and nonnegative
if abs(psi[0]) > 1e-9:
psi = psi * np.exp(-1j*np.angle(psi[0]))
alpha = float(np.real(psi[0]))
alpha = np.clip(alpha, -1, 1)
theta_deg = float(np.degrees(2*np.arccos(alpha)))
phi_deg = float(np.degrees(np.angle(psi[1]))) % 360
return theta_deg, phi_deg
def bloch_uncertainties(ex, ey, ez):
dx = np.sqrt(max(0.0, 1 - ex**2))
dy = np.sqrt(max(0.0, 1 - ey**2))
dz = np.sqrt(max(0.0, 1 - ez**2))
return dx, dy, dz
# ── Mesh of the Bloch sphere ─────────────────────────────────
def bloch_mesh():
u = np.linspace(0, 2*np.pi, 40)
v = np.linspace(0, np.pi, 20)
return (np.outer(np.cos(u), np.sin(v)),
np.outer(np.sin(u), np.sin(v)),
np.outer(np.ones_like(u), np.cos(v)))
xs, ys, zs = bloch_mesh()
# ── Build the initial traces (fixed indices; updates change only the data) ──
#
# [0] surface sphere
# [1] scatter3d line x-axis line
# [2] scatter3d text x label
# [3] scatter3d line y-axis line
# [4] scatter3d text y label
# [5] scatter3d line z-axis line
# [6] scatter3d text z label
# [7] scatter3d Bloch vector (current state, violet)
# [8] scatter3d dashed projection → x-axis (orange)
# [9] scatter3d dashed projection → y-axis (blue)
# [10] scatter3d dashed projection → z-axis (teal)
# [11] scatter3d vector after a quantum gate (grayish violet, initially hidden)
#
IDX_BV = 7
IDX_PX = 8
IDX_PY = 9
IDX_PZ = 10
IDX_GATE = 11
th0, ph0 = 90, 0 # initial state |+⟩
psi0 = angles_to_ket(th0, ph0)
ex0, ey0, ez0 = ket_to_bloch(psi0)
dx0, dy0, dz0 = bloch_uncertainties(ex0, ey0, ez0)
def make_all_traces(ex, ey, ez, ex_g=None, ey_g=None, ez_g=None):
surf = go.Surface(
x=xs, y=ys, z=zs,
colorscale=[[0, C_AREA], [1, C_AREA]],
showscale=False, opacity=0.18, hoverinfo='skip')
axes = []
for av, col, lab in [([1,0,0],C_WARN,'x'),([0,1,0],C_V2,'y'),([0,0,1],C_T1,'z')]:
axes.append(go.Scatter3d(
x=[-av[0]*1.3, av[0]*1.3],
y=[-av[1]*1.3, av[1]*1.3],
z=[-av[2]*1.3, av[2]*1.3],
mode='lines', line=dict(color=col, width=3),
hoverinfo='skip', showlegend=False))
axes.append(go.Scatter3d(
x=[av[0]*1.45], y=[av[1]*1.45], z=[av[2]*1.45],
mode='text', text=[f'<b>{lab}</b>'],
textfont=dict(color=col, size=14),
hoverinfo='skip', showlegend=False))
bv = go.Scatter3d(
x=[0, ex], y=[0, ey], z=[0, ez],
mode='lines+markers',
line=dict(color=C_V1, width=6),
marker=dict(size=[2, 10], color=C_V1),
name='Current state',
hovertemplate='Bloch point<br>(%{x:.3f}, %{y:.3f}, %{z:.3f})<extra></extra>')
px = go.Scatter3d(x=[ex, ex], y=[0, ey], z=[0, ez],
mode='lines', line=dict(color=C_WARN, width=2, dash='dash'),
hoverinfo='skip', showlegend=False)
py = go.Scatter3d(x=[0, ex], y=[ey, ey], z=[0, ez],
mode='lines', line=dict(color=C_V2, width=2, dash='dash'),
hoverinfo='skip', showlegend=False)
pz = go.Scatter3d(x=[0, ex], y=[0, ey], z=[ez, ez],
mode='lines', line=dict(color=C_T1, width=2, dash='dash'),
hoverinfo='skip', showlegend=False)
# vector after a quantum gate (initially hidden)
vis = ex_g is not None
gx = ex_g if vis else 0
gy = ey_g if vis else 0
gz = ez_g if vis else 0
gate_vec = go.Scatter3d(
x=[0, gx], y=[0, gy], z=[0, gz],
mode='lines+markers',
line=dict(color=C_AUX, width=4, dash='dot'),
marker=dict(size=[2, 8], color=C_AUX),
visible=vis,
name='State after gate',
hovertemplate='Bloch point after gate<br>(%{x:.3f}, %{y:.3f}, %{z:.3f})<extra></extra>')
return [surf] + axes + [bv, px, py, pz, gate_vec]
all_traces = make_all_traces(ex0, ey0, ez0)
fig = go.FigureWidget(data=all_traces)
fig.update_layout(
title=dict(text='Bloch Sphere · Quantum Gates · Uncertainty Principle',
font=dict(color='#000000', size=15)),
scene=dict(
bgcolor=C_BG,
xaxis=dict(range=[-1.6,1.6], showgrid=True, gridcolor=C_GRID,
zeroline=False, showticklabels=False, title=''),
yaxis=dict(range=[-1.6,1.6], showgrid=True, gridcolor=C_GRID,
zeroline=False, showticklabels=False, title=''),
zaxis=dict(range=[-1.6,1.6], showgrid=True, gridcolor=C_GRID,
zeroline=False, showticklabels=True,
tickvals=[-1,0,1], title=''),
aspectmode='cube',
camera=dict(eye=dict(x=1.5, y=1.5, z=0.8))),
paper_bgcolor=C_BG,
width=580, height=520,
margin=dict(l=0, r=0, t=40, b=0),
showlegend=False)
# ── Dashboard ─────────────────────────────────────────────────
dashboard = widgets.HTML()
def make_dashboard(ex, ey, ez, dx, dy, dz,
ex_g=None, ey_g=None, ez_g=None,
gate_name=None):
def fmt(v): return f'{v:+.4f}'
def ineq(lhs, rhs, label):
ok = '✓' if lhs >= rhs - 1e-9 else '✗'
col = '#2AD2C9' if ok == '✓' else '#FF5D47'
return (f'<tr><td style="padding:2px 6px">{label}</td>'
f'<td style="padding:2px 6px;text-align:right">{lhs:.4f}</td>'
f'<td>≥</td>'
f'<td style="padding:2px 6px;text-align:right">{rhs:.4f}</td>'
f'<td style="padding:2px 6px;color:{col};font-weight:bold">{ok}</td></tr>')
total = dx**2 + dy**2 + dz**2
gate_block = ''
if ex_g is not None:
dx_g, dy_g, dz_g = bloch_uncertainties(ex_g, ey_g, ez_g)
gate_block = f'''
<div style="margin:8px 0;border-top:1px solid {C_GRID}"></div>
<div style="color:{C_AUX};font-weight:bold;margin-bottom:6px">
⚡ State after gate ({gate_name})</div>
<table>
<tr><td style="padding:2px 6px">⟨σ<sub>x</sub>⟩</td>
<td style="padding:2px 6px;color:{C_WARN}">{fmt(ex_g)}</td>
<td style="padding:2px 6px">Δσ<sub>x</sub></td>
<td style="padding:2px 6px;color:{C_WARN}">{dx_g:.4f}</td></tr>
<tr><td style="padding:2px 6px">⟨σ<sub>y</sub>⟩</td>
<td style="padding:2px 6px;color:{C_V2}">{fmt(ey_g)}</td>
<td style="padding:2px 6px">Δσ<sub>y</sub></td>
<td style="padding:2px 6px;color:{C_V2}">{dy_g:.4f}</td></tr>
<tr><td style="padding:2px 6px">⟨σ<sub>z</sub>⟩</td>
<td style="padding:2px 6px;color:{C_T1}">{fmt(ez_g)}</td>
<td style="padding:2px 6px">Δσ<sub>z</sub></td>
<td style="padding:2px 6px;color:{C_T1}">{dz_g:.4f}</td></tr>
</table>'''
return f'''
<div style="font-family:monospace;background:{C_BG};border:1px solid {C_GRID};
border-radius:6px;padding:12px;width:290px;font-size:12.5px">
<div style="color:{C_V1};font-weight:bold;font-size:13px;margin-bottom:8px">
📐 Current state</div>
<table>
<tr><td style="padding:2px 6px">⟨σ<sub>x</sub>⟩</td>
<td style="padding:2px 6px;color:{C_WARN}">{fmt(ex)}</td>
<td style="padding:2px 6px">Δσ<sub>x</sub></td>
<td style="padding:2px 6px;color:{C_WARN}">{dx:.4f}</td></tr>
<tr><td style="padding:2px 6px">⟨σ<sub>y</sub>⟩</td>
<td style="padding:2px 6px;color:{C_V2}">{fmt(ey)}</td>
<td style="padding:2px 6px">Δσ<sub>y</sub></td>
<td style="padding:2px 6px;color:{C_V2}">{dy:.4f}</td></tr>
<tr><td style="padding:2px 6px">⟨σ<sub>z</sub>⟩</td>
<td style="padding:2px 6px;color:{C_T1}">{fmt(ez)}</td>
<td style="padding:2px 6px">Δσ<sub>z</sub></td>
<td style="padding:2px 6px;color:{C_T1}">{dz:.4f}</td></tr>
</table>
<div style="margin:6px 0;border-top:1px solid {C_GRID}"></div>
<div style="color:{C_V1};font-weight:bold;margin-bottom:4px">
🔒 Conserved quantity</div>
<div style="padding:2px 6px">
Δσ<sub>x</sub>²+Δσ<sub>y</sub>²+Δσ<sub>z</sub>² = <b>{total:.4f}</b>
</div>
<div style="margin:6px 0;border-top:1px solid {C_GRID}"></div>
<div style="color:{C_V1};font-weight:bold;margin-bottom:4px">
📏 Robertson-Schrödinger</div>
<table>
{ineq(dx*dy, abs(ez), 'Δσ<sub>x</sub>·Δσ<sub>y</sub>≥|⟨σ<sub>z</sub>⟩|')}
{ineq(dy*dz, abs(ex), 'Δσ<sub>y</sub>·Δσ<sub>z</sub>≥|⟨σ<sub>x</sub>⟩|')}
{ineq(dz*dx, abs(ey), 'Δσ<sub>z</sub>·Δσ<sub>x</sub>≥|⟨σ<sub>y</sub>⟩|')}
</table>
{gate_block}
</div>'''
dashboard.value = make_dashboard(ex0, ey0, ez0, dx0, dy0, dz0)
# ── State (avoids mutable global variables) ──────────────────
state = {'psi': psi0.copy(), 'gate_psi': None, 'gate_name': None}
def update_fig(psi, gate_psi=None, gate_name=None):
ex, ey, ez = ket_to_bloch(psi)
dx, dy, dz = bloch_uncertainties(ex, ey, ez)
ex_g = ey_g = ez_g = None
if gate_psi is not None:
ex_g, ey_g, ez_g = ket_to_bloch(gate_psi)
with fig.batch_update():
# [7] Bloch vector
fig.data[IDX_BV].x = [0, ex]
fig.data[IDX_BV].y = [0, ey]
fig.data[IDX_BV].z = [0, ez]
# [8] projection x
fig.data[IDX_PX].x = [ex, ex]
fig.data[IDX_PX].y = [0, ey]
fig.data[IDX_PX].z = [0, ez]
# [9] projection y
fig.data[IDX_PY].x = [0, ex]
fig.data[IDX_PY].y = [ey, ey]
fig.data[IDX_PY].z = [0, ez]
# [10] projection z
fig.data[IDX_PZ].x = [0, ex]
fig.data[IDX_PZ].y = [0, ey]
fig.data[IDX_PZ].z = [ez, ez]
# [11] vector after a quantum gate
if gate_psi is not None:
fig.data[IDX_GATE].x = [0, ex_g]
fig.data[IDX_GATE].y = [0, ey_g]
fig.data[IDX_GATE].z = [0, ez_g]
fig.data[IDX_GATE].visible = True
else:
fig.data[IDX_GATE].visible = False
dashboard.value = make_dashboard(
ex, ey, ez, dx, dy, dz, ex_g, ey_g, ez_g, gate_name)
# ── Sliders ──────────────────────────────────────────────────
slider_theta = widgets.IntSlider(
value=th0, min=0, max=180, step=1,
description='θ (polar):',
style={'description_width': '90px'},
layout=widgets.Layout(width='420px'),
continuous_update=True)
slider_phi = widgets.IntSlider(
value=ph0, min=0, max=360, step=1,
description='φ (azimuth):',
style={'description_width': '90px'},
layout=widgets.Layout(width='420px'),
continuous_update=True)
def on_slider(change):
psi = angles_to_ket(slider_theta.value, slider_phi.value)
state['psi'] = psi
state['gate_psi'] = None
state['gate_name'] = None
update_fig(psi)
slider_theta.observe(on_slider, names='value')
slider_phi.observe(on_slider, names='value')
# ── Preset-state buttons ─────────────────────────────────────
preset_params = [
('|0⟩ N pole', 0, 0),
('|1⟩ S pole', 180, 0),
('|+⟩ +x', 90, 0),
('|−⟩ −x', 90, 180),
('|+i⟩ +y', 90, 90),
('Equal Δσ', 54.74, 45),
]
def make_preset_btn(label, th, ph):
btn = widgets.Button(
description=label,
layout=widgets.Layout(width='108px', height='28px'),
style={'button_color': '#F4EEF9', 'font_size': '11.5px'})
def handler(b):
slider_theta.value = int(round(th))
slider_phi.value = int(round(ph))
btn.on_click(handler)
return btn
preset_btns = [make_preset_btn(l, th, ph) for l, th, ph in preset_params]
preset_row = widgets.HBox(preset_btns)
# ── Quantum gate buttons ─────────────────────────────────────
gate_btn_style = {'button_color': '#EAF9F8', 'font_size': '12px'}
gate_buttons = {}
for gname in ['H', 'S', 'T', 'X', 'Y', 'Z']:
btn = widgets.Button(
description=f'{gname} gate',
layout=widgets.Layout(width='72px', height='28px'),
style=gate_btn_style)
def make_gate_handler(name):
def handler(b):
U = GATES[name]
new_psi = U @ state['psi']
new_psi = new_psi / np.linalg.norm(new_psi)
state['gate_psi'] = new_psi
state['gate_name'] = name
# move the sliders to the position after the gate, keeping state['psi'] in sync
th_new, ph_new = ket_to_angles(new_psi)
state['psi'] = new_psi
# unobserve temporarily so that on_slider is not triggered and gate_psi is not cleared
slider_theta.unobserve(on_slider, names='value')
slider_phi.unobserve(on_slider, names='value')
slider_theta.value = int(round(th_new))
slider_phi.value = int(round(ph_new))
slider_theta.observe(on_slider, names='value')
slider_phi.observe(on_slider, names='value')
# show both vectors, "before → after"
old_psi = angles_to_ket(
int(round(np.degrees(2*np.arccos(np.clip(float(np.real(
state['psi'][0]/np.linalg.norm(state['psi'])
)),-1,1))))),
int(round(ph_new)))
update_fig(new_psi, gate_psi=new_psi, gate_name=name)
return handler
btn.on_click(make_gate_handler(gname))
gate_buttons[gname] = btn
gate_label = widgets.Label('Gates:', layout=widgets.Layout(width='60px'))
gate_row = widgets.HBox([gate_label] + list(gate_buttons.values()))
# ── Overall layout ───────────────────────────────────────────
controls = widgets.VBox([
widgets.Label('── Slider controls ──'),
slider_theta,
slider_phi,
widgets.Label('── Preset states ──'),
preset_row,
widgets.Label('── Apply a quantum gate (updates the current state) ──'),
gate_row,
])
main_row = widgets.HBox(
[fig, dashboard],
layout=widgets.Layout(align_items='flex-start'))
widgets.VBox([controls, main_row])
Part 2: Observation and Reflection¶
On the Geometry of the Bloch Sphere:¶
Set to 0° or 180°. Which operator has zero uncertainty? What is the lower bound in the Robertson-Schrödinger inequality in this case?
Find the position at which the three uncertainties are exactly equal, and confirm the value of the conserved quantity .
Starting from , keep clicking the H gate. Between which two positions does the Bloch point go back and forth? Why?
On the Geometric Action of Quantum Gates:¶
Starting from (the north pole), click H → S → H in order. What is the final state? What about applying H → S → H once more? How is this related to clicking X directly? (Hint: on the Bloch sphere, is a 90° rotation about the -axis; applying it twice gives , an important identity in quantum computation.)
Starting from (the -axis on the equator), click S and T separately and observe the change in the azimuthal angle . By how many degrees do the S gate and the T gate rotate, respectively?
Click the X gate. What transformation does the Bloch point undergo? How is this related to its name, “bit-flip gate”?
On the Uncertainty Principle:¶
Find a state for which equality holds in the Robertson-Schrödinger inequality . How many such states are there? How are they distributed on the Bloch sphere?
Quantum gates keep the radius of the Bloch sphere equal to 1 (unitary evolution does not change the purity). What does this mean physically?