Skip to content

Commit 9efbf58

Browse files
Merge 3710327 into 96490a3
2 parents 96490a3 + 3710327 commit 9efbf58

8 files changed

Lines changed: 425 additions & 31 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Version of the specification language/format, to manage future changes
2+
spec_version = "1.0"
3+
# Version of this particular benchmark suite, to manage any important changes
4+
suite_version = "0.1"
5+
# Unique identifier for this benchmark suite
6+
id = "fake_backend_5Q_benchmarks"
7+
# Human readable description
8+
description = "Compiler benchmarks running in small quantum device emulators."
9+
10+
# --------- Compilers ----------
11+
# For now, restricting to qiskit derived compilers to work with the same target-device options
12+
[[compilers]]
13+
id = "ucc"
14+
[[compilers]]
15+
id = "qiskit-default"
16+
17+
# --------- Target Devices ----------
18+
[[target_devices]]
19+
# 5 qubits
20+
id = "ibm_fake_manila"
21+
22+
# --------- Benchmarks ----------
23+
24+
[[benchmarks]]
25+
id = "mqt:qaoa_N5"
26+
description = "mqt:qaoa circuit (N=5)"
27+
simulate.measurement = "qaoa"
28+
generator.name = "mqt:qaoa"
29+
generator.params.N = 5
30+
31+
32+
33+
[[benchmarks]]
34+
id = "mqt:qft_N5"
35+
description = "mqt:qft circuit (N=5)"
36+
simulate.measurement = "uniform_superposition_projector"
37+
generator.name = "mqt:qft"
38+
generator.params.N = 5
39+
40+
41+
42+
[[benchmarks]]
43+
id = "prep_select_N5"
44+
description = "prep_select circuit (N=5)"
45+
simulate.measurement = "prep_select_all_ones"
46+
generator.name = "prep_select"
47+
generator.params.N = 5
48+
49+
50+
51+
[[benchmarks]]
52+
id = "qcnn_N5"
53+
description = "qcnn circuit (N=5)"
54+
simulate.measurement = "qcnn"
55+
generator.name = "qcnn"
56+
generator.params.N = 5
57+
58+
59+
60+
[[benchmarks]]
61+
id = "qft_N5"
62+
description = "qft circuit (N=5)"
63+
simulate.measurement = "uniform_superposition_projector"
64+
generator.name = "qft"
65+
generator.params.N = 5
66+
67+
68+
69+
[[benchmarks]]
70+
id = "qv_N5"
71+
description = "qv circuit (N=5)"
72+
simulate.measurement = "heavy_output"
73+
generator.name = "qv"
74+
generator.params.N = 5
75+
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Version of the specification language/format, to manage future changes
2+
spec_version = "1.0"
3+
# Version of this particular benchmark suite, to manage any important changes
4+
suite_version = "0.1"
5+
# Unique identifier for this benchmark suite
6+
id = "fake_backend_7Q_benchmarks"
7+
# Human readable description
8+
description = "Compiler benchmarks running in small quantum device emulators."
9+
10+
# --------- Compilers ----------
11+
# For now, restricting to qiskit derived compilers to work with the same target-device options
12+
[[compilers]]
13+
id = "ucc"
14+
[[compilers]]
15+
id = "qiskit-default"
16+
17+
# --------- Target Devices ----------
18+
[[target_devices]]
19+
# 7 qubits
20+
id = "ibm_fake_jakarta"
21+
22+
23+
# --------- Benchmarks ----------
24+
# Change all these to N=7 versions of the existing 5Q benchmarks
25+
26+
# --------- Benchmarks ----------
27+
[[benchmarks]]
28+
id = "mqt:qaoa_N7"
29+
description = "mqt:qaoa circuit (N=7)"
30+
simulate.measurement = "qaoa"
31+
generator.name = "mqt:qaoa"
32+
generator.params.N = 7
33+
34+
[[benchmarks]]
35+
id = "mqt:qft_N7"
36+
description = "mqt:qft circuit (N=7)"
37+
simulate.measurement = "uniform_superposition_projector"
38+
generator.name = "mqt:qft"
39+
generator.params.N = 7
40+
41+
[[benchmarks]]
42+
id = "prep_select_N7"
43+
description = "prep_select circuit (N=7)"
44+
simulate.measurement = "prep_select_all_ones"
45+
generator.name = "prep_select"
46+
generator.params.N = 7
47+
48+
[[benchmarks]]
49+
id = "qcnn_N7"
50+
description = "qcnn circuit (N=7)"
51+
simulate.measurement = "qcnn"
52+
generator.name = "qcnn"
53+
generator.params.N = 7
54+
55+
[[benchmarks]]
56+
id = "qft_N7"
57+
description = "qft circuit (N=7)"
58+
simulate.measurement = "uniform_superposition_projector"
59+
generator.name = "qft"
60+
generator.params.N = 7
61+
62+
[[benchmarks]]
63+
id = "qv_N7"
64+
description = "qv circuit (N=7)"
65+
simulate.measurement = "heavy_output"
66+
generator.name = "qv"
67+
generator.params.N = 7
68+
69+

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ dependencies = [
2424
"seaborn>=0.13.2",
2525
"tomlkit>=0.13.3",
2626
"mqt-bench>=2.1.0",
27+
"pre-commit>=4.4.0",
2728
]
2829

2930
[build-system]

src/ucc_bench/runner.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import logging
66
from logging import LoggerAdapter
77
from concurrent.futures import ProcessPoolExecutor
8+
89
from .suite import BenchmarkSuite, BenchmarkSpec
910
from .compilers import BaseCompiler, DEFAULT_GATESET
1011
from .results import BenchmarkResult, CompilerInfo, CompilationMetrics
@@ -18,7 +19,7 @@
1819
from qiskit.transpiler import Target
1920
from qiskit_aer import AerSimulator
2021
from qiskit_aer.noise import NoiseModel
21-
from .utils import validate_circuit_gates
22+
from .utils import validate_circuit_gates, strip_measurements
2223
from concurrent.futures import as_completed
2324
from qiskit.qasm3 import dumps
2425

@@ -123,13 +124,26 @@ def process(self, msg, kwargs):
123124

124125
if register.has_observable(benchmark.simulate.measurement):
125126
observable = register.get_observable(benchmark.simulate.measurement)
126-
simulation_metrics = calc_expectation_value(
127-
observable(raw_circuit_qiskit.num_qubits),
128-
raw_circuit_qiskit,
129-
compiled_circuit_qiskit,
130-
simulator,
131-
)
132-
simulation_metrics.measurement_id = observable._id
127+
# If an output metric is registered, use it
128+
if register.has_output_metric(observable._id):
129+
output_metric = register.get_output_metric(observable._id)
130+
simulation_metrics = output_metric(
131+
raw_circuit_qiskit,
132+
compiled_circuit_qiskit,
133+
simulator,
134+
)
135+
simulation_metrics.measurement_id = observable._id
136+
else:
137+
# When calculating expectation values, strip measurements first
138+
strip_measurements(raw_circuit_qiskit)
139+
strip_measurements(compiled_circuit_qiskit)
140+
simulation_metrics = calc_expectation_value(
141+
observable(raw_circuit_qiskit.num_qubits),
142+
raw_circuit_qiskit,
143+
compiled_circuit_qiskit,
144+
simulator,
145+
)
146+
simulation_metrics.measurement_id = observable._id
133147
elif register.has_output_metric(benchmark.simulate.measurement):
134148
output_metric = register.get_output_metric(benchmark.simulate.measurement)
135149
simulation_metrics = output_metric(

src/ucc_bench/simulation/observables.py

Lines changed: 86 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,60 @@
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+
158
"""
259
This module provides functionality calculating expectation values of compiled circuits
360
with and without noise. It has some common functions for calculating these values given an
@@ -8,15 +65,6 @@
865
in the circuit as an argument and return a Qiskit Operator representing the observable to measure.
966
"""
1067

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-
2068
# ----------------------------------------------------
2169
# Simulation functions to calculate expectation values
2270
# ----------------------------------------------------
@@ -151,14 +199,25 @@ def generate_square_heisenberg_observable(num_qubits):
151199

152200
@register.observable("qaoa")
153201
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+
162221
"""
163222
pauli_strings = []
164223
# Weights of edges between vertices and of the resulting unitary evolution
@@ -188,15 +247,20 @@ def generate_qaoa_observable(num_qubits):
188247
(7, 9, 4.265),
189248
(8, 9, 1.690),
190249
]
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:
193259
pauli_string = ["I"] * num_qubits
194-
# Place Z operators on the chosen qubits
195260
pauli_string[i] = "Z"
196261
pauli_string[j] = "Z"
197-
# Convert to PauliSumOp
198262
pauli_strings.append("".join(pauli_string))
199-
coeffs = [weight for _, _, weight in weighted_edges]
263+
coeffs = [weight for _, _, weight in filtered_edges]
200264
observable = SparsePauliOp(pauli_strings, coeffs)
201265
return observable
202266

src/ucc_bench/utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,13 @@ def validate_circuit_gates(circuit, allowed_gates=None):
3737
)
3838

3939
return qc
40+
41+
42+
# Statevector-based simulation
43+
def strip_measurements(circuit):
44+
# Remove measurement instructions and classical bits
45+
circuit.data = [instr for instr in circuit.data if instr[0].name != "measure"]
46+
if circuit.num_clbits > 0:
47+
# Remove all classical registers
48+
circuit._clbits = []
49+
circuit.cregs = []

tests/test_qaoa_observable.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import pytest
2+
3+
from ucc_bench.simulation.observables import generate_qaoa_observable
4+
5+
6+
def test_qaoa_observable_filters_edges_for_small_num_qubits():
7+
obs_5 = generate_qaoa_observable(5)
8+
# All pauli strings should be length 5
9+
assert all(len(pstr) == 5 for pstr, _ in obs_5.to_list()), (
10+
"Pauli strings must match num_qubits"
11+
)
12+
# Each term is ZZ on some pair; so each string must have exactly two Z's
13+
for pstr, coeff in obs_5.to_list():
14+
assert pstr.count("Z") == 2, f"Each term should be a ZZ; got {pstr}" # noqa: S101
15+
16+
17+
def test_qaoa_observable_minimum_qubits():
18+
with pytest.raises(ValueError):
19+
generate_qaoa_observable(1)
20+
21+
22+
def test_qaoa_observable_full_10_qubits():
23+
obs_10 = generate_qaoa_observable(10)
24+
# Expect length equal to number of weighted edges in original list
25+
original_edge_count = 24
26+
assert len(obs_10.to_list()) == original_edge_count
27+
# Spot check one known coefficient value
28+
coeffs = [c for _, c in obs_10.to_list()]
29+
assert pytest.approx(6.720) in coeffs

0 commit comments

Comments
 (0)