QCQI — Chapter 3: Multiple Qubits, Entanglement & Measurement
Goal: build and analyze 2-qubit states, reduced states (partial trace), Bell correlations, and CHSH — across multiple frameworks (non-IBM by default).
Set backend below to one of: cirq, pennylane, braket, pyquil, qiskit.
# Backend selector
backend = "backend" # change: "pennylane", "braket", "pyquil", "qiskit"
print("Selected backend:", backend)
# Optional installs (if needed)
# !pip install cirq pennylane amazon-braket-sdk pyquil qiskit qiskit-aerSelected backend: backend
Utilities: states, partial trace, entropy, concurrence (pure 2-qubit)¶
import numpy as np
def bell_phi_plus():
psi = (1/np.sqrt(2))*np.array([[1,0,0,1]], dtype=complex).T
return psi / np.linalg.norm(psi)
def rho_from_psi(psi): return psi @ psi.conj().T
def ptrace_rhoA(rho):
# basis ordering: 00,01,10,11
R00 = rho[0:2, 0:2]
R11 = rho[2:4, 2:4]
return R00 + R11
def von_neumann_entropy(rho, base=2):
w = np.linalg.eigvalsh(rho)
w = w[w > 1e-14]
return float(-(w*np.log(w)/np.log(base)).sum())
def concurrence_pure(a,b,c,d):
return float(np.clip(2*abs(a*d - b*c), 0, 1))
Example: Bell state reduced states & entropy¶
psi = bell_phi_plus()
rho = rho_from_psi(psi)
rhoA = ptrace_rhoA(rho)
print("rho_A =", np.round(rhoA,3))
print("S(rho_A) =", round(von_neumann_entropy(rhoA), 4))
rho_A = [[0.5+0.j 0. +0.j]
[0. +0.j 0.5+0.j]]
S(rho_A) = 1.0
CHSH setup (angles and correlators)¶
# Measure along axes in XY-plane with angles alpha (Alice) and beta (Bob) relative to X
def rot_to_z(theta):
# rotate measurement axis in XY-plane to Z then measure Z (Rz then Ry)
# For XY-plane axis at angle theta from X, measurement operator is cos(theta) X + sin(theta) Y
# To map to Z measurement, apply Rz(-theta) then Rx(-pi/2); implement via Ry equivalents for frameworks as needed.
return thetaFramework demos¶
Cirq: Bell + CHSH sampling¶
if backend == "cirq":
import cirq, numpy as np
q0, q1 = cirq.LineQubit.range(2)
def bell_circuit():
c = cirq.Circuit()
c.append([cirq.H(q0), cirq.CNOT(q0, q1)])
return c
def measure_axis(circ, a_angle, b_angle, shots=2000):
c = circ.copy()
# rotate XY-axis to Z: Rz(-theta) then Rx(-pi/2) maps X to Z; implement with Rx and Rz
c.append([cirq.rz(-a_angle)(q0), cirq.rx(-np.pi/2)(q0)])
c.append([cirq.rz(-b_angle)(q1), cirq.rx(-np.pi/2)(q1)])
c.append([cirq.measure(q0, key='a'), cirq.measure(q1, key='b')])
sim = cirq.Simulator()
res = sim.run(c, repetitions=shots)
A = 1 - 2*res.measurements['a'][:,0] # map {0,1}->{+1,-1}
B = 1 - 2*res.measurements['b'][:,0]
return float(np.mean(A*B))
circ = bell_circuit()
# Near-optimal CHSH angles (XY-plane): 0, pi/2 for Alice; pi/4, -pi/4 for Bob
A0, A1 = 0.0, np.pi/2
B0, B1 = np.pi/4, -np.pi/4
E00 = measure_axis(circ, A0, B0)
E01 = measure_axis(circ, A0, B1)
E10 = measure_axis(circ, A1, B0)
E11 = measure_axis(circ, A1, B1)
S = E00 + E01 + E10 - E11
print("E00,E01,E10,E11 =", [round(x,3) for x in [E00,E01,E10,E11]])
print("CHSH S ~", round(S,3))
E00,E01,E10,E11 = [-0.689, -0.73, 0.707, -0.698]
CHSH S ~ -0.014
PennyLane: Bell expvals & CHSH (deterministic expvals)¶
if backend == "pennylane":
import pennylane as qml, numpy as np
dev = qml.device("default.qubit", wires=2, shots=None)
def measure_corr(alpha, beta):
@qml.qnode(dev)
def circuit():
qml.H(0); qml.CNOT(wires=[0,1])
# rotate measurement axes in XY plane: observable cos t X + sin t Y
A = np.cos(alpha)*qml.PauliX(0) + np.sin(alpha)*qml.PauliY(0)
B = np.cos(beta)*qml.PauliX(1) + np.sin(beta)*qml.PauliY(1)
return qml.expval(A @ B)
return circuit()
A0, A1 = 0.0, np.pi/2
B0, B1 = np.pi/4, -np.pi/4
E00 = measure_corr(A0,B0); E01 = measure_corr(A0,B1)
E10 = measure_corr(A1,B0); E11 = measure_corr(A1,B1)
S = E00 + E01 + E10 - E11
print("E00,E01,E10,E11 =", [round(float(x),3) for x in [E00,E01,E10,E11]])
print("CHSH S ~", round(float(S),3))
E00,E01,E10,E11 = [0.707, 0.707, -0.707, 0.707]
CHSH S ~ 0.0
Braket: LocalSimulator sampling with axis rotations¶
if backend == "braket":
from braket.circuits import Circuit
from braket.devices import LocalSimulator
import numpy as np
def bell():
return Circuit().h(0).cnot(0,1)
def est(alpha, beta, shots=2000):
# rotate XY axis to Z: Rz(-theta) then Rx(-pi/2)
circ = bell().rz(0, float(-alpha)).rx(0, float(-np.pi/2)).rz(1, float(-beta)).rx(1, float(-np.pi/2))
circ = circ.measure(0).measure(1)
dev = LocalSimulator()
res = dev.run(circ, shots=shots).result().measurement_counts
N = sum(res.values())
def bit_to_pm1(x): return 1 if x=='0' else -1
E = 0.0
for bitstr, count in res.items():
a = bit_to_pm1(bitstr[-1]); b = bit_to_pm1(bitstr[-2]) # qubit0 -> last char
E += a*b*count
return E/N
A0, A1 = 0.0, np.pi/2
B0, B1 = np.pi/4, -np.pi/4
E00 = est(A0,B0); E01 = est(A0,B1); E10 = est(A1,B0); E11 = est(A1,B1)
S = E00 + E01 + E10 - E11
print("E00,E01,E10,E11 =", [round(x,3) for x in [E00,E01,E10,E11]])
print("CHSH S ~", round(S,3))
E00,E01,E10,E11 = [-0.712, -0.718, 0.688, -0.726]
CHSH S ~ -0.016
PyQuil: QVM sampling¶
if backend == "pyquil":
from pyquil import Program
from pyquil.gates import H, CNOT, RZ, RX, MEASURE
from pyquil.pyqvm import PyQVM
import numpy as np
def est(alpha, beta, shots=2000):
p = Program()
ro = p.declare('ro','BIT',2)
p += H(0); p += CNOT(0,1)
p += RZ(-alpha, 0); p += RX(-np.pi/2, 0)
p += RZ(-beta, 1); p += RX(-np.pi/2, 1)
p += MEASURE(0, ro[0]); p += MEASURE(1, ro[1])
qvm = PyQVM(n_qubits = 2)
qvm.execute(p.wrap_in_numshots_loop(shots))
res = qvm.read_memory(region_name = 'ro')
A = 1 - 2*res[:,0]; B = 1 - 2*res[:,1]
return float(np.mean(A*B))
A0, A1 = 0.0, np.pi/2
B0, B1 = np.pi/4, -np.pi/4
vals = [est(A0,B0), est(A0,B1), est(A1,B0), est(A1,B1)]
print("E00,E01,E10,E11 =", [round(v,3) for v in vals])
print("CHSH S ~", round(vals[0]+vals[1]+vals[2]-vals[3],3))
E00,E01,E10,E11 = [-0.7, -0.719, 0.727, -0.735]
CHSH S ~ 0.043
Qiskit: Aer sampling¶
if backend == "qiskit":
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
import numpy as np
sim = AerSimulator()
def est(alpha, beta, shots=2000):
qc = QuantumCircuit(2,2)
qc.h(0); qc.cx(0,1)
qc.rz(-float(alpha), 0); qc.rx(-np.pi/2, 0)
qc.rz(-float(beta), 1); qc.rx(-np.pi/2, 1)
qc.measure([0,1],[0,1])
res = sim.run(qc, shots=shots).result().get_counts()
N = sum(res.values())
E = 0.0
for bitstr, count in res.items():
a = 1 if bitstr[-1]=='0' else -1
b = 1 if bitstr[-2]=='0' else -1
E += a*b*count
return E/N
A0, A1 = 0.0, np.pi/2
B0, B1 = np.pi/4, -np.pi/4
E00 = est(A0,B0); E01 = est(A0,B1); E10 = est(A1,B0); E11 = est(A1,B1)
S = E00 + E01 + E10 - E11
print("E00,E01,E10,E11 =", [round(x,3) for x in [E00,E01,E10,E11]])
print("CHSH S ~", round(S,3))
E00,E01,E10,E11 = [-0.711, -0.688, 0.693, -0.706]
CHSH S ~ -0.0
Exercises¶
Implement a function that returns the concurrence for any pure 2-qubit state and verify values for each Bell state.
Add depolarizing noise
pto the Bell circuit and plot CHSHS(p); find the threshold whereSfalls below 2.Prepare partially entangled states
cos θ |00⟩ + sin θ |11⟩and study howSvaries withθ.