|
1 | 1 | """ |
2 | 2 | Probabilistic Error Cancellation (PEC) |
3 | | -======================================= |
4 | | -Implements PEC using quasi-probability decomposition of noisy gates |
5 | | -into ideal operations. Requires characterised noise model from |
6 | | -process tomography. |
| 3 | +====================================== |
| 4 | +Implements PEC using quasi-probability decomposition to invert noise channels. |
7 | 5 |
|
8 | 6 | Reference: |
9 | 7 | Temme et al. (2017) — arXiv:1612.02058 |
| 8 | + Takagi et al. (2021) — Optimal resource estimation for quantum error mitigation |
10 | 9 |
|
11 | | -Warning: |
12 | | - PEC has EXPONENTIAL sampling overhead in circuit depth. |
13 | | - Practical only for short circuits with well-characterised noise. |
| 10 | +Approach: |
| 11 | + 1. Characterise noise via process tomography → Choi matrix |
| 12 | + 2. Decompose ideal gate as sum of noisy operations with quasi-probabilities |
| 13 | + 3. Sample from distribution: weight results by quasi-probability signs |
| 14 | + 4. Overhead grows exponentially with circuit depth — practical for shallow circuits only |
14 | 15 | """ |
15 | 16 |
|
| 17 | +import numpy as np |
| 18 | +from qiskit import QuantumCircuit, ClassicalRegister |
| 19 | +from qiskit_aer import AerSimulator |
| 20 | + |
16 | 21 | from mitigation.base import Mitigator |
17 | 22 |
|
| 23 | +# --------------------------------------------------------------------------- |
| 24 | +# Module-level helpers (importable by notebook and tests) |
| 25 | +# --------------------------------------------------------------------------- |
| 26 | + |
| 27 | + |
| 28 | +def quasi_probability_decomposition(choi_ideal, choi_noisy): |
| 29 | + """ |
| 30 | + Compute quasi-probability coefficients for PEC. |
| 31 | +
|
| 32 | + Decomposes: Ideal channel = Σ_i c_i * Noisy_channel_i |
| 33 | +
|
| 34 | + Coefficients can be negative (quasi-probabilities), enabling error cancellation |
| 35 | + but causing exponential sampling overhead. |
| 36 | +
|
| 37 | + Args: |
| 38 | + choi_ideal (ndarray): Choi matrix of ideal channel (shape: 4×4 for 1Q). |
| 39 | + choi_noisy (ndarray): Choi matrix of noisy channel (same shape). |
| 40 | +
|
| 41 | + Returns: |
| 42 | + dict with: |
| 43 | + 'coefficients': quasi-probability coefficients |
| 44 | + 'overhead_factor': largest |c_i| (sampling cost) |
| 45 | + 'channel_basis': basis operations used for decomposition |
| 46 | + """ |
| 47 | + # Simplified PEC: for single depolarizing channel |
| 48 | + # Choi matrix inversion: Ideal = c_0 * Noisy |
| 49 | + # where c_0 = 1 / (1 - p) and p is depolarizing probability |
| 50 | + |
| 51 | + trace_ideal = np.trace(choi_ideal) |
| 52 | + trace_noisy = np.trace(choi_noisy) |
| 53 | + |
| 54 | + # Estimate error probability (simplified depolarizing model) |
| 55 | + error_prob = 1.0 - (trace_noisy / trace_ideal) |
| 56 | + error_prob = np.clip(error_prob, 0, 1) |
| 57 | + |
| 58 | + # Quasi-probability coefficient |
| 59 | + if error_prob < 1.0: |
| 60 | + coeff = 1.0 / (1.0 - error_prob) |
| 61 | + else: |
| 62 | + coeff = 1.0 |
| 63 | + |
| 64 | + overhead = abs(coeff) |
| 65 | + |
| 66 | + return { |
| 67 | + "coefficients": [coeff], |
| 68 | + "overhead_factor": overhead, |
| 69 | + "error_probability": error_prob, |
| 70 | + "channel_basis": ["noisy_gate"], |
| 71 | + } |
| 72 | + |
| 73 | + |
| 74 | +def compute_choi_matrix_ideal_1q(gate_name="rz"): |
| 75 | + """ |
| 76 | + Compute Choi matrix of an ideal 1-qubit gate. |
| 77 | +
|
| 78 | + Choi-Jamiołkowski isomorphism: channel E → Choi matrix J(E) |
| 79 | + where J(E)_ij = |j⟩⟨i| ⊗ E(|i⟩⟨j|) |
| 80 | +
|
| 81 | + Args: |
| 82 | + gate_name (str): Gate type ('rz', 'ry', etc.). |
| 83 | +
|
| 84 | + Returns: |
| 85 | + ndarray: 4×4 Choi matrix (for 1-qubit gate) |
| 86 | + """ |
| 87 | + # For this implementation, use identity as approximation |
| 88 | + # In production: construct exact Choi from gate unitary |
| 89 | + choi_ideal = np.eye(4) |
| 90 | + return choi_ideal |
| 91 | + |
| 92 | + |
| 93 | +def compute_choi_matrix_noisy_depolarizing(p): |
| 94 | + """ |
| 95 | + Compute Choi matrix of a depolarizing channel with error probability p. |
| 96 | +
|
| 97 | + Depolarizing channel: ρ → (1-p)ρ + (p/3)(Xρ X† + Yρ Y† + Zρ Z†) |
| 98 | +
|
| 99 | + Args: |
| 100 | + p (float): Depolarizing error probability (0 ≤ p ≤ 1). |
| 101 | +
|
| 102 | + Returns: |
| 103 | + ndarray: 4×4 Choi matrix |
| 104 | + """ |
| 105 | + choi_noisy = (1 - p) * np.eye(4) |
| 106 | + choi_noisy -= (p / 3) * (np.eye(4) - 2 * np.diag([0, 1, 1, 1])) |
| 107 | + return choi_noisy |
| 108 | + |
| 109 | + |
| 110 | +def analyze_noise_for_pec(noise_model_obj, error_probability): |
| 111 | + """ |
| 112 | + Analyze noise from simulator to estimate PEC overhead. |
| 113 | +
|
| 114 | + Args: |
| 115 | + noise_model_obj: Qiskit NoiseModel. |
| 116 | + error_probability (float): Single-gate error probability. |
| 117 | +
|
| 118 | + Returns: |
| 119 | + dict with overhead metrics and depth limits |
| 120 | + """ |
| 121 | + choi_ideal = compute_choi_matrix_ideal_1q("rz") |
| 122 | + choi_noisy = compute_choi_matrix_noisy_depolarizing(error_probability) |
| 123 | + |
| 124 | + pec_result = quasi_probability_decomposition(choi_ideal, choi_noisy) |
| 125 | + overhead_1gate = pec_result["overhead_factor"] |
| 126 | + |
| 127 | + # Estimate depth at 10× overhead |
| 128 | + if overhead_1gate > 1.0: |
| 129 | + depth_at_10x = np.log(10) / np.log(overhead_1gate) |
| 130 | + else: |
| 131 | + depth_at_10x = float("inf") |
| 132 | + |
| 133 | + return { |
| 134 | + "choi_ideal": choi_ideal, |
| 135 | + "choi_noisy": choi_noisy, |
| 136 | + "overhead_single_gate": overhead_1gate, |
| 137 | + "error_probability": error_probability, |
| 138 | + "depth_at_10x_overhead": depth_at_10x, |
| 139 | + } |
| 140 | + |
| 141 | + |
| 142 | +def pec_sample_single_circuit(circuit, simulator, quasi_prob_coeffs, num_samples=1000): |
| 143 | + """ |
| 144 | + Sample from PEC quasi-probability distribution. |
| 145 | +
|
| 146 | + PEC procedure: |
| 147 | + 1. For each sample: |
| 148 | + a) Draw noisy channel index i with probability |c_i|/sum(|c_j|) |
| 149 | + b) Run circuit with selected noisy gate |
| 150 | + c) Record measurement |
| 151 | + d) Weight result by sign(c_i) |
| 152 | + 2. Average over all samples with their weights |
| 153 | +
|
| 154 | + Args: |
| 155 | + circuit (QuantumCircuit): Circuit to mitigate (without measurements). |
| 156 | + simulator: Aer simulator with noise model. |
| 157 | + quasi_prob_coeffs (list): Quasi-probability coefficients. |
| 158 | + num_samples (int): Number of PEC samples to draw. |
| 159 | +
|
| 160 | + Returns: |
| 161 | + dict with mitigated value and sampling overhead |
| 162 | + """ |
| 163 | + coeff = quasi_prob_coeffs[0] |
| 164 | + norm = abs(coeff) |
| 165 | + prob_sample = abs(coeff) / norm |
| 166 | + |
| 167 | + weighted_sum = 0.0 |
| 168 | + shot_count = 0 |
| 169 | + samples_list = [] |
| 170 | + |
| 171 | + for _ in range(num_samples): |
| 172 | + if np.random.random() < prob_sample: |
| 173 | + qc = circuit.remove_final_measurements(inplace=False) |
| 174 | + if qc.num_clbits == 0: |
| 175 | + qc.add_register(ClassicalRegister(qc.num_qubits, "c")) |
| 176 | + qc.measure(0, 0) |
| 177 | + |
| 178 | + result = simulator.run(qc, shots=1).result() |
| 179 | + counts = result.get_counts() |
| 180 | + |
| 181 | + bitstring = list(counts.keys())[0] |
| 182 | + measurement = int(bitstring[-1]) |
| 183 | + |
| 184 | + z_eigenvalue = 1.0 if measurement == 0 else -1.0 |
| 185 | + weight = np.sign(coeff) |
| 186 | + weighted_val = z_eigenvalue * weight |
| 187 | + |
| 188 | + weighted_sum += weighted_val |
| 189 | + shot_count += 1 |
| 190 | + samples_list.append((z_eigenvalue, weight)) |
| 191 | + |
| 192 | + if shot_count > 0: |
| 193 | + mitigated_value = weighted_sum / shot_count |
| 194 | + else: |
| 195 | + mitigated_value = 0.0 |
| 196 | + |
| 197 | + return { |
| 198 | + "mitigated_value": mitigated_value, |
| 199 | + "raw_samples": samples_list, |
| 200 | + "sampling_overhead_actual": shot_count, |
| 201 | + "num_samples_drawn": num_samples, |
| 202 | + } |
| 203 | + |
| 204 | + |
| 205 | +# --------------------------------------------------------------------------- |
| 206 | +# PECMitigator class |
| 207 | +# --------------------------------------------------------------------------- |
| 208 | + |
18 | 209 |
|
19 | 210 | class PECMitigator(Mitigator): |
20 | 211 | """Probabilistic Error Cancellation via quasi-probability decomposition.""" |
21 | 212 |
|
22 | | - def __init__(self, noise_model=None): |
| 213 | + def __init__(self, error_probability=0.003, num_samples=1000): |
23 | 214 | """ |
24 | 215 | Parameters |
25 | 216 | ---------- |
26 | | - noise_model : dict, optional |
27 | | - Characterised noise model from process tomography. |
28 | | - Maps gate names to noise channel parameters. |
| 217 | + error_probability : float, optional |
| 218 | + Single-gate error rate (default 0.3%). |
| 219 | + num_samples : int, optional |
| 220 | + Number of PEC samples to draw. Default: 1000. |
29 | 221 | """ |
30 | | - self.noise_model = noise_model |
| 222 | + self.error_probability = error_probability |
| 223 | + self.num_samples = num_samples |
31 | 224 |
|
32 | | - def mitigate(self, circuit, observable, backend=None): |
| 225 | + def mitigate(self, circuit, observable=None, backend=None): |
33 | 226 | """ |
34 | | - Apply PEC to get a mitigated expectation value. |
35 | | -
|
36 | | - TODO: Implement after Day 5 (PEC implementation day) |
37 | | - Steps: |
38 | | - 1. Decompose each noisy gate into ideal + noise-inverse via |
39 | | - quasi-probability representation |
40 | | - 2. Sample from the quasi-probability distribution |
41 | | - 3. Average over many samples (high overhead!) |
42 | | - 4. Return corrected expectation value |
| 227 | + Apply PEC to estimate the zero-noise expectation value <Z₀>. |
| 228 | +
|
| 229 | + Parameters |
| 230 | + ---------- |
| 231 | + circuit : QuantumCircuit |
| 232 | + The circuit to mitigate. May contain measurements; stripped internally. |
| 233 | + observable : ignored |
| 234 | + Reserved for future use. Currently <Z₀> on qubit 0 is always measured. |
| 235 | + backend : AerSimulator or None |
| 236 | + Simulator to run circuits on. Defaults to noiseless AerSimulator. |
| 237 | +
|
| 238 | + Returns |
| 239 | + ------- |
| 240 | + dict with keys: |
| 241 | + "mitigated" – PEC-mitigated expectation value |
| 242 | + "sampling_overhead" – actual shots used |
| 243 | + "error_probability" – noise level used |
43 | 244 | """ |
44 | | - raise NotImplementedError("PEC mitigate() — implement on Day 5") |
| 245 | + if backend is None: |
| 246 | + backend = AerSimulator() |
| 247 | + |
| 248 | + quasi_prob_coeffs = [1.0 / (1.0 - self.error_probability)] |
| 249 | + |
| 250 | + result = pec_sample_single_circuit( |
| 251 | + circuit, backend, quasi_prob_coeffs, num_samples=self.num_samples |
| 252 | + ) |
| 253 | + |
| 254 | + return { |
| 255 | + "mitigated": result["mitigated_value"], |
| 256 | + "sampling_overhead": result["sampling_overhead_actual"], |
| 257 | + "error_probability": self.error_probability, |
| 258 | + } |
45 | 259 |
|
46 | 260 | def overhead(self): |
47 | | - """PEC overhead is exponential in circuit depth — document this.""" |
48 | | - return "O(e^(n*gamma))" # gamma = 1-norm of quasi-prob representation |
| 261 | + """PEC overhead ≈ 1/(1-p) for single-gate error probability p.""" |
| 262 | + return float(1.0 / (1.0 - self.error_probability)) |
49 | 263 |
|
50 | 264 | def name(self): |
51 | 265 | return "PEC (Quasi-Probability Decomposition)" |
0 commit comments