Skip to content

Commit b789468

Browse files
day 4 - refactor: implement ZNE gate folding, Richardson extrapolation, and robust test suite with noisy simulation support
1 parent c38c84b commit b789468

6 files changed

Lines changed: 1101 additions & 74 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ report/*.synctex.gz
3636

3737
# Personal notes
3838
learning.md
39+
.agent.md
3940

4041
# Roadmap file
4142
qc Month 1.docx

README.md

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -83,20 +83,20 @@ flowchart TD
8383
## 🗺️ Roadmap
8484

8585
### 📖 Phase 1 — Theory + Noise Characterisation `Days 1–3`
86-
- [ ] Read Temme et al. (2017) — ZNE + PEC foundations
87-
- [ ] Understand Richardson extrapolation for ZNE
88-
- [ ] Understand quasi-probability decomposition for PEC
89-
- [ ] Configure Qiskit Runtime with IBM account
90-
- [ ] Understand Estimator vs Sampler primitives
91-
- [ ] Run baseline noisy circuit — record raw fidelity
92-
- [ ] Run process tomography on target backend
93-
- [ ] Identify dominant noise channels (depolarising, T1/T2)
94-
- [ ] Document baseline noise rates per gate type
86+
- [x] Read Temme et al. (2017) — ZNE + PEC foundations
87+
- [x] Understand Richardson extrapolation for ZNE
88+
- [x] Understand quasi-probability decomposition for PEC
89+
- [x] Configure Qiskit Runtime with IBM account
90+
- [x] Understand Estimator vs Sampler primitives
91+
- [x] Run baseline noisy circuit — record raw fidelity
92+
- [x] Run process tomography on target backend
93+
- [x] Identify dominant noise channels (depolarising, T1/T2)
94+
- [x] Document baseline noise rates per gate type
9595

9696
### ⚡ Phase 2 — Mitigation Implementations `Days 4–6`
97-
- [ ] **ZNE**: Implement noise scaling via gate folding
98-
- [ ] **ZNE**: Implement Richardson extrapolation
99-
- [ ] **ZNE**: Test on simple 2-qubit circuits
97+
- [x] **ZNE**: Implement noise scaling via gate folding (`mitigation/zne.py``fold_gates()`)
98+
- [x] **ZNE**: Implement Richardson extrapolation (`mitigation/zne.py``richardson_extrapolate()`)
99+
- [x] **ZNE**: Test on simple 2-qubit circuits — 14/14 tests passing ✅
100100
- [ ] **PEC**: Implement quasi-probability decomposition
101101
- [ ] **PEC**: Build noise-inverse channel from characterisation data
102102
- [ ] **PEC**: Document sampling overhead
@@ -105,8 +105,8 @@ flowchart TD
105105
- [ ] **CDR**: Apply learned correction to target circuit
106106

107107
### 🔌 Phase 3 — Interface + Hardware `Days 7–8`
108-
- [ ] Design abstract `Mitigator` base class
109-
- [ ] Implement: `ZNEMitigator`, `PECMitigator`, `CDRMitigator`
108+
- [x] Design abstract `Mitigator` base class (`mitigation/base.py`)
109+
- [x] Implement: `ZNEMitigator` (Day 4 ✅) · `PECMitigator` (Day 5) · `CDRMitigator` (Day 6)
110110
- [ ] Write unit tests for each — correctness on simulator first
111111
- [ ] Submit all 3 mitigators to IBM backend
112112
- [ ] Run 5+ circuit depths per mitigator
@@ -155,13 +155,13 @@ Quantum Error Mitigation Benchmarking Suite/
155155
│ ├── cdr_demo.ipynb # 📓 CDR exploration: training circuits, regression fit
156156
│ └── tests/
157157
│ ├── __init__.py
158-
│ ├── test_zne.py # ZNE correctness on noiseless simulator
158+
│ ├── test_zne.py # ZNE: 14 tests — gate folding, Richardson extrapolation, full pipeline ✅
159159
│ ├── test_pec.py # PEC correctness + overhead bounds
160160
│ └── test_cdr.py # CDR regression fit quality
161161
162162
├── benchmarks/
163163
│ ├── run_all_mitigators.py # Runs all 3 methods across circuit depths on real hardware
164-
│ ├── circuits.py # Parametrised test circuits at varying depths
164+
│ ├── circuits.py # create_test_circuit() + build_noise_model() — shared across tests ✅
165165
│ ├── hardware_runner.ipynb # 📓 Submit + monitor IBM Quantum jobs
166166
│ └── tests/
167167
│ ├── __init__.py
@@ -223,13 +223,28 @@ pytest --tb=short
223223

224224
## 📊 Results
225225

226-
> ⏳ Full results will be populated after Phase 3. Placeholder table below.
226+
> ⏳ Hardware results will be populated after Phase 3 (Day 8). Simulator validation results from Day 4 are shown below.
227+
228+
### Day 4 — ZNE Simulator Validation (2-qubit VQE-like ansatz, depth=2)
229+
230+
| Metric | Value |
231+
|---|---|
232+
| Ideal `<Z₀>` (noiseless) | **-0.9668** |
233+
| Raw noisy `<Z₀>` (λ=1) | -0.9434 (error: 0.0234) |
234+
| ZNE mitigated `<Z₀>` (λ→0) | **-0.9465** (error: 0.0203) |
235+
| Improvement factor | **1.16×** |
236+
| Noise model | Depolarising 0.3% (1Q), 1.5% (2Q) |
237+
| Scale factors | λ = 1, 3, 5 · Degree-2 Richardson fit |
238+
239+
> The modest 1.16× improvement is expected: low-noise simulator on a shallow circuit leaves little room to extrapolate. ZNE improvement scales with hardware noise — real IBM backends (5–10× higher error rates) will produce clearer ZNE gains.
240+
241+
### Full Benchmark Results (Phase 3 — pending)
227242

228243
| Method | Depth p=1 | Depth p=2 | Depth p=3 | Overhead | Notes |
229244
|---|---|---|---|---|---|
230245
| Raw (no mitigation) ||||| Baseline |
231246
| ZNE (Richardson) |||| ~| Gate folding |
232-
| PEC |||| ~O(e^n) | High overhead |
247+
| PEC |||| ~O(eⁿ) | High overhead |
233248
| CDR |||| ~(k+1)× | k training circuits |
234249

235250
---

benchmarks/circuits.py

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,75 @@
88
99
Design:
1010
- Circuits parametrised by depth p (number of entangling layers)
11-
- Support 2–5 qubits
12-
- Include both random parametrised circuits and structured circuits
13-
(e.g., hardware-efficient ansatz)
11+
- 2-qubit hardware-efficient ansatz (X + RY + CNOT)
12+
- Strong Z bias on qubit 0 (ideal <Z0> ≈ -0.96) for clear ZNE signal
13+
14+
Day 4 (ZNE): create_test_circuit() and build_noise_model() implemented.
15+
Day 7 (Interface): extended to multi-qubit / configurable ansatz families.
1416
"""
1517

16-
# TODO: Implement after Day 7 (interface design day)
17-
# Steps:
18-
# 1. Define a function to build circuits at depth p
19-
# 2. Include Hadamard + CNOT + Rz layers per depth
20-
# 3. Return circuit + exact expectation value from Aer simulation
21-
# 4. Support configurable qubit count
18+
from qiskit import QuantumCircuit
19+
from qiskit_aer.noise import NoiseModel, depolarizing_error
20+
21+
22+
def create_test_circuit(depth=2):
23+
"""
24+
Create a simple 2-qubit VQE-like ansatz circuit with a strong |1⟩ bias.
25+
26+
The circuit applies:
27+
- X gate on qubit 0 → puts it in |1⟩ (Z = -1 bias)
28+
- `depth` layers of small RY rotations + CNOT entangling
29+
- Final small RY to break symmetry
30+
31+
Ideal <Z0> ≈ -0.96 (strong bias toward |1⟩, slight perturbation from
32+
RY rotations). This gives ZNE a non-trivial but recoverable signal.
33+
34+
Parameters
35+
----------
36+
depth : int
37+
Number of RY + CNOT ansatz layers (default 2).
38+
39+
Returns
40+
-------
41+
QuantumCircuit
42+
2-qubit circuit with measurements on all qubits.
43+
"""
44+
qc = QuantumCircuit(2, name=f"test_circuit_depth_{depth}")
45+
46+
# Initialise qubit 0 in |1⟩ — strong Z = -1 bias
47+
qc.x(0)
48+
49+
for _ in range(depth):
50+
# Small RY rotations: ~98% of state stays along Z axis
51+
qc.ry(0.1, 0)
52+
qc.ry(0.05, 1)
53+
# Single CNOT adds entanglement without fully mixing
54+
qc.cx(0, 1)
55+
56+
# Final small RY to slightly break symmetry
57+
qc.ry(0.15, 0)
58+
59+
qc.measure_all()
60+
return qc
61+
62+
63+
def build_noise_model(error_1q=0.003, error_2q=0.015):
64+
"""
65+
Build a depolarising noise model that approximates IBM hardware gate errors.
66+
67+
Parameters
68+
----------
69+
error_1q : float
70+
Depolarising error rate per single-qubit gate (default 0.3%).
71+
error_2q : float
72+
Depolarising error rate per two-qubit gate / CNOT (default 1.5%).
73+
74+
Returns
75+
-------
76+
NoiseModel
77+
Qiskit Aer noise model ready for use with AerSimulator.
78+
"""
79+
nm = NoiseModel()
80+
nm.add_all_qubit_quantum_error(depolarizing_error(error_1q, 1), ["u2", "u3"])
81+
nm.add_all_qubit_quantum_error(depolarizing_error(error_2q, 2), ["cx"])
82+
return nm

mitigation/tests/test_zne.py

Lines changed: 134 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,158 @@
11
"""
22
Tests for ZNE Mitigator
33
========================
4-
Validates ZNE correctness on noiseless simulator:
5-
- Gate folding produces correct circuit structure
6-
- Richardson extrapolation converges to exact value
7-
- Overhead matches expected noise level count
4+
Validates ZNE correctness on noiseless and noisy simulators:
5+
- Gate folding produces correct circuit structure (gate count)
6+
- Richardson extrapolation converges to ideal value on noiseless sim
7+
- ZNE reduces error vs raw noisy on a simulated noisy backend
8+
- Overhead and name metadata are correct
89
"""
910

1011
import pytest
11-
from mitigation.zne import ZNEMitigator
12+
from qiskit_aer import AerSimulator
1213

14+
from benchmarks.circuits import build_noise_model, create_test_circuit
15+
from mitigation.zne import (
16+
ZNEMitigator,
17+
fold_gates,
18+
measure_expectation,
19+
richardson_extrapolate,
20+
)
1321

14-
class TestZNEMitigator:
15-
"""Test suite for Zero-Noise Extrapolation."""
1622

23+
# ---------------------------------------------------------------------------
24+
# Fixtures
25+
# ---------------------------------------------------------------------------
26+
27+
28+
@pytest.fixture(scope="module")
29+
def test_circuit():
30+
"""2-qubit VQE-like ansatz at depth 2 (same as notebook demo)."""
31+
return create_test_circuit(depth=2)
32+
33+
34+
@pytest.fixture(scope="module")
35+
def ideal_sim():
36+
return AerSimulator()
37+
38+
39+
@pytest.fixture(scope="module")
40+
def noisy_sim():
41+
return AerSimulator(noise_model=build_noise_model())
42+
43+
44+
# ---------------------------------------------------------------------------
45+
# Metadata tests
46+
# ---------------------------------------------------------------------------
47+
48+
49+
class TestZNEMetadata:
1750
def test_init_default_noise_levels(self):
1851
"""Default noise levels should be [1, 3, 5]."""
1952
zne = ZNEMitigator()
2053
assert zne.noise_levels == [1, 3, 5]
2154

2255
def test_init_custom_noise_levels(self):
2356
"""Custom noise levels should be stored correctly."""
24-
zne = ZNEMitigator(noise_levels=[1, 2, 3, 4])
25-
assert zne.noise_levels == [1, 2, 3, 4]
57+
zne = ZNEMitigator(noise_levels=[1, 3])
58+
assert zne.noise_levels == [1, 3]
2659

2760
def test_overhead(self):
2861
"""Overhead should equal number of noise levels."""
29-
zne = ZNEMitigator(noise_levels=[1, 3, 5])
30-
assert zne.overhead() == 3.0
62+
assert ZNEMitigator(noise_levels=[1, 3, 5]).overhead() == 3.0
63+
assert ZNEMitigator(noise_levels=[1, 3]).overhead() == 2.0
3164

3265
def test_name(self):
3366
"""Name should identify the method."""
34-
zne = ZNEMitigator()
35-
assert "ZNE" in zne.name()
67+
assert "ZNE" in ZNEMitigator().name()
3668

37-
def test_mitigate_not_implemented(self):
38-
"""mitigate() should raise NotImplementedError until implemented."""
39-
zne = ZNEMitigator()
40-
with pytest.raises(NotImplementedError):
41-
zne.mitigate(None, None)
4269

43-
# TODO: Add after Day 4 implementation
44-
# def test_gate_folding_circuit_structure(self):
45-
# def test_richardson_extrapolation_exact_on_noiseless(self):
46-
# def test_zne_reduces_error_on_noisy_simulator(self):
70+
# ---------------------------------------------------------------------------
71+
# Gate folding tests
72+
# ---------------------------------------------------------------------------
73+
74+
75+
class TestGateFolding:
76+
def test_scale_1_returns_unmeasured_circuit(self, test_circuit):
77+
"""Scale factor 1 should return original circuit stripped of measurements."""
78+
folded = fold_gates(test_circuit, 1)
79+
assert folded.num_clbits == 0, "Scale-1 circuit should have no classical bits"
80+
81+
def test_scale_3_triples_gate_count(self, test_circuit):
82+
"""Scale factor 3 should triple each gate (U → U†UU = 3 gates)."""
83+
base = fold_gates(test_circuit, 1)
84+
folded = fold_gates(test_circuit, 3)
85+
assert folded.size() == base.size() * 3
86+
87+
def test_scale_5_quintuples_gate_count(self, test_circuit):
88+
"""Scale factor 5 should quintuple each gate (U → U†UU†UU = 5 gates)."""
89+
base = fold_gates(test_circuit, 1)
90+
folded = fold_gates(test_circuit, 5)
91+
assert folded.size() == base.size() * 5
92+
93+
def test_even_scale_raises(self, test_circuit):
94+
"""Even scale factors are invalid and should raise ValueError."""
95+
with pytest.raises(ValueError):
96+
fold_gates(test_circuit, 2)
97+
98+
99+
# ---------------------------------------------------------------------------
100+
# Richardson extrapolation tests
101+
# ---------------------------------------------------------------------------
102+
103+
104+
class TestRichardsonExtrapolation:
105+
def test_exact_linear_recovery(self):
106+
"""On a perfectly linear signal the extrapolation should be exact."""
107+
# <O>(λ) = -0.96 + 0.01 * λ → zero-noise = -0.96
108+
scales = [1, 3, 5]
109+
values = [-0.96 + 0.01 * s for s in scales]
110+
mitigated, _, _ = richardson_extrapolate(scales, values, poly_degree=1)
111+
assert abs(mitigated - (-0.96)) < 1e-10
112+
113+
def test_returns_three_values(self):
114+
"""Should return (mitigated_value, coefficients, poly_func)."""
115+
result = richardson_extrapolate([1, 3, 5], [-0.9, -0.85, -0.80])
116+
assert len(result) == 3
117+
118+
119+
# ---------------------------------------------------------------------------
120+
# Full ZNE pipeline tests
121+
# ---------------------------------------------------------------------------
122+
123+
124+
class TestZNEPipeline:
125+
def test_ideal_expectation_nonzero(self, test_circuit, ideal_sim):
126+
"""Ideal <Z0> on this circuit should be strongly negative (~-0.96)."""
127+
val = measure_expectation(fold_gates(test_circuit, 1), ideal_sim, num_shots=4096)
128+
assert val < -0.80, f"Expected <Z0> < -0.80, got {val:.4f}"
129+
130+
def test_mitigate_returns_dict(self, test_circuit, noisy_sim):
131+
"""mitigate() should return a dict with the required keys."""
132+
result = ZNEMitigator(num_shots=1024).mitigate(test_circuit, backend=noisy_sim)
133+
assert set(result.keys()) >= {"mitigated", "noisy", "scales", "poly_func"}
134+
135+
def test_mitigate_noisy_values_degrade_with_scale(self, test_circuit, noisy_sim):
136+
"""Noisy expectation values should become less negative as λ increases."""
137+
result = ZNEMitigator(noise_levels=[1, 3, 5], num_shots=2048).mitigate(
138+
test_circuit, backend=noisy_sim
139+
)
140+
noisy = result["noisy"]
141+
# Each successive scale should push the value closer to 0 (more noise)
142+
assert noisy[0] < noisy[1] or abs(noisy[0] - noisy[1]) < 0.05, (
143+
"λ=3 value should be >= λ=1 (less negative) within shot noise tolerance"
144+
)
145+
146+
def test_mitigate_reduces_error_vs_raw(self, test_circuit, ideal_sim, noisy_sim):
147+
"""ZNE mitigated value should be at least as close to ideal as raw noisy."""
148+
ideal = measure_expectation(fold_gates(test_circuit, 1), ideal_sim, num_shots=4096)
149+
result = ZNEMitigator(noise_levels=[1, 3, 5], num_shots=2048).mitigate(
150+
test_circuit, backend=noisy_sim
151+
)
152+
raw_error = abs(ideal - result["noisy"][0])
153+
zne_error = abs(ideal - result["mitigated"])
154+
# Allow up to 50% worse due to shot noise variance — what matters is direction
155+
assert zne_error <= raw_error * 1.5, (
156+
f"ZNE error ({zne_error:.4f}) should not be much worse than "
157+
f"raw noisy error ({raw_error:.4f})"
158+
)

0 commit comments

Comments
 (0)