Notebook 14 — Shor breaks RSA#
The moment the book has been building toward. We run Shor’s algorithm on N=15, 21, 35, see it succeed, fail, retry, and finally factor.
import numpy as np
from pqc_edu.quantum.shor import factor
One quick success: N=15#
Seed chosen so the first few attempts succeed.
rng = np.random.default_rng(0)
result = factor(15, rng, max_attempts=10)
print('factors:', result.factors)
print('quantum runs:', result.total_quantum_runs)
print('attempts:')
for i, att in enumerate(result.attempts):
print(f' {i+1}. a={att.a}, measured={att.measured}, r={att.inferred_period}, status={att.status}')
factors: (3, 5)
quantum runs: 1
attempts:
1. a=13, measured=64, r=4, status=success
Note: simulation shortcut#
We implement the \(a^x \bmod N\) step as a state-vector update over all basis states at once, rather than building a polynomial-size reversible arithmetic circuit. The algorithmic behavior (creating the superposition \(\sum_x |x\rangle|a^x \bmod N\rangle\)) is identical; only the classical work done by our simulator is exponential. This is a simulation shortcut, not an algorithmic one — a real quantum computer would use modular-exponentiation gates that scale polynomially in \(n\).
N=21 and N=35#
Larger circuits; more counting qubits help resolve the period.
for N in (21, 35):
rng = np.random.default_rng(1 + N)
result = factor(N, rng, max_attempts=20)
print(f'\nN={N}: factors = {result.factors} (after {result.total_quantum_runs} quantum runs, {len(result.attempts)} total attempts)')
for i, att in enumerate(result.attempts):
print(f' {i+1}. a={att.a}, measured={att.measured}, r={att.inferred_period}, status={att.status}')
N=21: factors = (7, 3) (after 2 quantum runs, 3 total attempts)
1. a=16, measured=0, r=None, status=period_not_found
2. a=8, measured=0, r=None, status=period_not_found
3. a=7, measured=-1, r=None, status=a_not_coprime
N=35: factors = (7, 5) (after 0 quantum runs, 1 total attempts)
1. a=14, measured=-1, r=None, status=a_not_coprime
Success rate over many seeds#
Shor is probabilistic — each run picks a random \(a\). Some choices fail (odd period, trivial square root, …). We run it 100 times with different seeds and count how often a single attempt suffices.
first_try_success = 0
success_eventually = 0
total = 100
for seed in range(total):
rng = np.random.default_rng(seed)
result = factor(15, rng, max_attempts=10)
if result.factors is not None:
success_eventually += 1
if len(result.attempts) == 1:
first_try_success += 1
print(f'N=15 success rate (first try): {first_try_success}/{total}')
print(f'N=15 success rate (within 10): {success_eventually}/{total}')
N=15 success rate (first try): 66/100
N=15 success rate (within 10): 100/100
Status breakdown#
What happens when it fails?
from collections import Counter
all_statuses = Counter()
for seed in range(total):
rng = np.random.default_rng(seed)
result = factor(15, rng, max_attempts=10)
for att in result.attempts:
all_statuses[att.status] += 1
print('attempt outcomes across 100 seeds of N=15:')
for s, c in sorted(all_statuses.items(), key=lambda kv: -kv[1]):
print(f' {s:22s} {c}')
attempt outcomes across 100 seeds of N=15:
a_not_coprime 67
period_not_found 37
success 33
trivial_factor 13
RSA scaling note#
ML-KEM-512 replaces RSA-2048 and above. RSA-2048 requires ~4096 qubits for Shor. Our simulator maxes out around 14 qubits (N ≤ 35). The gap between “factor 35 in a simulator” and “factor \(2^{2048}\)” is about 10 orders of magnitude in problem size. This is why Q-Day is still a debate: building a 4000-qubit fault-tolerant quantum computer is vastly harder than anything built today.
→ Final chapter: 15_shor_vs_lattice.ipynb — why this trick does not break ML-KEM.