EE798V Course Project — Qiskit Transpiler Pipeline Study
This project benchmarks Qiskit's transpiler pipeline across optimization levels 0–5 on two quantum algorithms (Grover's Search and QFT), measuring how compilation quality affects circuit structure and output fidelity under realistic noise.
Core thesis: Better compilation = sharper probability peaks at correct answer states and fewer noisy gates. The primary quality metric is TVD (Total Variation Distance) between ideal statevector probabilities and noisy simulation output.
Requirements: Python 3.11+
pip install qiskit>=1.0 qiskit-aer>=0.14 qiskit-ibm-runtime>=0.20 matplotlib numpy
# Optional (for level 5):
pip install z3-solver # Level 5: HoareOptimizerStep 1 — Transpile and simulate (generates metrics JSON and QPY files in results/):
# Run for each algorithm and qubit count you want to plot
python compiler/transpile_all.py --algorithm grover --n_qubits 4 --max_level 5
python compiler/transpile_all.py --algorithm qft --n_qubits 4 --max_level 5
# For qubit scaling plots, also run for 3, 5, 6 qubits
for n in 2 3 4 5 ; do
python compiler/transpile_all.py --algorithm grover --n_qubits $n --max_level 5
python compiler/transpile_all.py --algorithm qft --n_qubits $n --max_level 5
doneStep 2 — Generate all three plots:
python plots/plots.pyAll three plots are generated by plots/plots.py and saved to results/.
| Plot | Output file | Description |
|---|---|---|
| 1. Histogram | results/histogram_4q.png |
Output state probability distribution for 4 qubits at L0 vs L5. Ideal (blue) vs noisy (red) side by side for both algorithms. |
| 2. Metrics vs Level | results/metrics_vs_level.png |
Gate count and circuit depth vs optimization level (0–5) for both algorithms at 4 qubits. |
| 3. Qubit Scaling | results/qubit_scaling.png |
Gate count, CX count, and circuit depth vs qubit count (3–6) for optimization levels 0, 3, 5. |
# With custom qubit count for plots 1 and 2:
python plots/plots.py --n_qubits 5
# Interactive display instead of saving:
python plots/plots.py --showLevels 0–3 use Qiskit's built-in presets. Levels 4–5 are custom extensions that clone Level 3 and add progressively more aggressive passes.
| Level | Layout | Routing | Optimization |
|---|---|---|---|
| 0 | TrivialLayout | BasicSwap | None |
| 1 | Trivial → DenseLayout | BasicSwap | Optimize1qGates |
| 2 | DenseLayout → VF2Layout | LookaheadSwap | CommutativeCancellation, CXCancellation |
| 3 | SabreLayout | SabreSwap | Full peephole + UnitarySynthesis (KAK) |
| 4 | SabreLayout | SabreSwap | Level 3 + TemplateOptimization + diagonal removal |
| 5 | SabreLayout | SabreSwap | Level 4 + HoareOptimizer (z3-solver required) |
All stochastic passes are seeded (seed_transpiler=42, seed_simulator=42) for reproducible results.
| Algorithm | File | Ideal Output |
|---|---|---|
| Grover's Search | circuits/grover.py |
Sharp peak at marked state |
| Quantum Fourier Transform | circuits/qft.py |
Uniform distribution |
Both expose build(n_qubits, **kwargs) → QuantumCircuit.
TVD = 0.5 × Σₛ |P_ideal(s) − P_noisy(s)|
- TVD = 0 → noisy output matches ideal exactly (perfect compilation)
- TVD = 1 → no overlap between ideal and noisy distributions
TVD is the primary quality signal because it directly corresponds to how peaked vs spread the probability histogram looks. Secondary structural metrics (gate count, CX count, depth) are proxies that predict TVD.
from qiskit_ibm_runtime.fake_provider import FakeNairobiV2 # 7 qubits, real IBM noise dataBasis gate set: ['cx', 'rz', 'sx', 'x']. Statevector simulation is skipped for circuits with more than 20 qubits; those report structural metrics only (TVD = None).
circuits/ → compiler/ → hardware/ → analysis/ + plots/
build circuit transpile_all.py simulate w/ noise metrics + figures
(levels 0–5, serialise
to results/*.qpy)
Each stage is independent. Compiled circuits are serialised to results/*.qpy via qiskit.qpy, so transpilation runs once and plotting reads cached results.
quantum-compiler/
├── circuits/
│ ├── __init__.py # Registry: build_circuit(algorithm, n_qubits)
│ ├── grover.py # Grover's search
│ └── qft.py # Quantum Fourier Transform
├── compiler/
│ ├── __init__.py
│ ├── transpile_all.py # Top-level runner (CLI entry point)
│ ├── preset_pass_managers.py # Levels 0–5 PassManagers
│ └── pass_manager.py # PassManager / AnalysisPass / TransformationPass
├── hardware/
│ ├── __init__.py
│ ├── backends.py # Backend selection (FakeNairobi / FakeSherbrooke)
│ └── noise_models.py # Base + scaled noise models
├── analysis/
│ ├── __init__.py
│ ├── metrics.py # TVD, gate_count, cx_count, depth, compute_all_metrics
│ └── benchmark.py # Sweep all qubit counts → benchmark_report.json
├── plots/
│ ├── __init__.py
│ └── plots.py # All three benchmark plots
└── results/ # Generated outputs (QPY, JSON, PNG)
All outputs land in results/:
| File pattern | Description |
|---|---|
<algo>_<n>q_level<l>.qpy |
Compiled circuit in QPY format |
<algo>_<n>q_metrics.json |
Gate count, CX count, depth, TVD, and probability dicts per level |
histogram_<n>q.png |
State probability histograms: L0 vs L5 for both algorithms |
metrics_vs_level.png |
Gate count + depth vs optimization level for both algorithms |
qubit_scaling.png |
Gate count, CX, depth vs qubit count for both algorithms |