|
| 1 | +from typing import Union |
| 2 | +from math import sqrt |
| 3 | +from qiskit import QuantumCircuit, ClassicalRegister |
| 4 | +from qiskit.quantum_info import Operator, Statevector, SparsePauliOp |
| 5 | +from qiskit_aer import AerSimulator |
| 6 | +import numpy as np |
| 7 | +from qiskit.result import Counts |
| 8 | +from ..registry import register |
| 9 | +from ..results import SimulationMetrics |
| 10 | + |
| 11 | + |
| 12 | +def calc_computational_basis_expectation( |
| 13 | + uncompiled_circuit: QuantumCircuit, |
| 14 | + compiled_circuit: QuantumCircuit, |
| 15 | + simulator: AerSimulator, |
| 16 | +) -> SimulationMetrics: |
| 17 | + # Ensure classical bits and measurement |
| 18 | + def ensure_classical_bits_and_measurement(circuit): |
| 19 | + num_qubits = circuit.num_qubits |
| 20 | + if circuit.num_clbits < num_qubits: |
| 21 | + cr = ClassicalRegister(num_qubits - circuit.num_clbits) |
| 22 | + circuit.add_register(cr) |
| 23 | + if not any(instr[0].name == "measure" for instr in circuit.data): |
| 24 | + circuit.measure(range(num_qubits), range(num_qubits)) |
| 25 | + |
| 26 | + ensure_classical_bits_and_measurement(uncompiled_circuit) |
| 27 | + ensure_classical_bits_and_measurement(compiled_circuit) |
| 28 | + |
| 29 | + shots = 1024 |
| 30 | + ideal_result = simulator.run(uncompiled_circuit, shots=shots).result() |
| 31 | + ideal_counts = ideal_result.get_counts() |
| 32 | + noisy_result = simulator.run(compiled_circuit, shots=shots).result() |
| 33 | + noisy_counts = noisy_result.get_counts() |
| 34 | + |
| 35 | + def z_expectation(counts: Counts, num_qubits: int): |
| 36 | + total = 0 |
| 37 | + shots = sum(counts.values()) |
| 38 | + for bitstring, count in counts.items(): |
| 39 | + val = 1 if bitstring.count("1") % 2 == 0 else -1 |
| 40 | + total += val * count |
| 41 | + return total / shots if shots > 0 else 0.0 |
| 42 | + |
| 43 | + uncompiled_ideal = z_expectation(ideal_counts, uncompiled_circuit.num_qubits) |
| 44 | + compiled_noisy = z_expectation(noisy_counts, compiled_circuit.num_qubits) |
| 45 | + |
| 46 | + import math |
| 47 | + |
| 48 | + return SimulationMetrics( |
| 49 | + uncompiled_ideal=uncompiled_ideal, |
| 50 | + compiled_ideal=math.nan, |
| 51 | + uncompiled_noisy=math.nan, |
| 52 | + compiled_noisy=compiled_noisy, |
| 53 | + ) |
| 54 | + |
| 55 | + |
| 56 | +register.output_metric("computational_basis")(calc_computational_basis_expectation) |
| 57 | + |
1 | 58 | """ |
2 | 59 | This module provides functionality calculating expectation values of compiled circuits |
3 | 60 | with and without noise. It has some common functions for calculating these values given an |
|
8 | 65 | in the circuit as an argument and return a Qiskit Operator representing the observable to measure. |
9 | 66 | """ |
10 | 67 |
|
11 | | -from typing import Union |
12 | | -from math import sqrt |
13 | | -from qiskit import QuantumCircuit |
14 | | -from qiskit.quantum_info import Operator, Statevector, SparsePauliOp |
15 | | -from qiskit_aer import AerSimulator |
16 | | -import numpy as np |
17 | | -from ..registry import register |
18 | | -from ..results import SimulationMetrics |
19 | | - |
20 | 68 | # ---------------------------------------------------- |
21 | 69 | # Simulation functions to calculate expectation values |
22 | 70 | # ---------------------------------------------------- |
@@ -151,14 +199,25 @@ def generate_square_heisenberg_observable(num_qubits): |
151 | 199 |
|
152 | 200 | @register.observable("qaoa") |
153 | 201 | def generate_qaoa_observable(num_qubits): |
154 | | - """Generates the problem Hamiltonian as the observable for the QAOA |
155 | | - benchmarking circuits, based on the binary encoding described in |
156 | | - Franz G. Fuchs, Herman Øie Kolden, Niels Henrik Aase, and Giorgio |
157 | | - Sartor "Efficient encoding of the weighted MAX k-CUT on a quantum computer |
158 | | - using QAOA". (2020) arXiv 2009.01095 (https://arxiv.org/abs/2009.01095). |
159 | | - The weights of the edges between vertices and of the resulting unitary |
160 | | - evolution come from the 10-vertex Barabasi-Albert graph in Fig 4(c) |
161 | | - of the paper. |
| 202 | + """Generate the problem Hamiltonian observable for QAOA benchmarks. |
| 203 | +
|
| 204 | + Notes |
| 205 | + ----- |
| 206 | + The original reference graph (Fuchs et al. 2020, arXiv:2009.01095) uses a |
| 207 | + 10-vertex Barabasi-Albert instance. The hard-coded edge list below encodes |
| 208 | + that graph. Some benchmark suites in this project request QAOA circuits |
| 209 | + with fewer than 10 qubits (e.g. N=5 or N=7). In those cases, the original |
| 210 | + edge list contains vertex indices that exceed ``num_qubits - 1`` which |
| 211 | + previously caused an ``IndexError: list assignment index out of range``. |
| 212 | +
|
| 213 | + To make the observable generation work for ``num_qubits < 10`` we filter |
| 214 | + out edges that touch vertices outside the available range. |
| 215 | +
|
| 216 | + If fewer than 2 valid vertices remain after filtering (i.e. ``num_qubits < 2``) |
| 217 | + we raise a ValueError because no meaningful 2-local ZZ Hamiltonian can be |
| 218 | + constructed. |
| 219 | + TODO: In general, we should define an observable for QAOA which allows an arbitrary number of qubits and generates the corresponding graph structure. |
| 220 | +
|
162 | 221 | """ |
163 | 222 | pauli_strings = [] |
164 | 223 | # Weights of edges between vertices and of the resulting unitary evolution |
@@ -188,15 +247,20 @@ def generate_qaoa_observable(num_qubits): |
188 | 247 | (7, 9, 4.265), |
189 | 248 | (8, 9, 1.690), |
190 | 249 | ] |
191 | | - for i, j, _ in weighted_edges: |
192 | | - # Start with identity string |
| 250 | + if num_qubits < 2: |
| 251 | + raise ValueError( |
| 252 | + f"QAOA observable requires at least 2 qubits; got num_qubits={num_qubits}" |
| 253 | + ) |
| 254 | + # Filter edges to those within the available qubit index range |
| 255 | + filtered_edges = [ |
| 256 | + (i, j, w) for (i, j, w) in weighted_edges if i < num_qubits and j < num_qubits |
| 257 | + ] |
| 258 | + for i, j, weight in filtered_edges: |
193 | 259 | pauli_string = ["I"] * num_qubits |
194 | | - # Place Z operators on the chosen qubits |
195 | 260 | pauli_string[i] = "Z" |
196 | 261 | pauli_string[j] = "Z" |
197 | | - # Convert to PauliSumOp |
198 | 262 | pauli_strings.append("".join(pauli_string)) |
199 | | - coeffs = [weight for _, _, weight in weighted_edges] |
| 263 | + coeffs = [weight for _, _, weight in filtered_edges] |
200 | 264 | observable = SparsePauliOp(pauli_strings, coeffs) |
201 | 265 | return observable |
202 | 266 |
|
|
0 commit comments