iqm-benchmarks 1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of iqm-benchmarks might be problematic. Click here for more details.
- iqm/benchmarks/__init__.py +31 -0
- iqm/benchmarks/benchmark.py +109 -0
- iqm/benchmarks/benchmark_definition.py +264 -0
- iqm/benchmarks/benchmark_experiment.py +163 -0
- iqm/benchmarks/compressive_gst/__init__.py +20 -0
- iqm/benchmarks/compressive_gst/compressive_gst.py +1029 -0
- iqm/benchmarks/entanglement/__init__.py +18 -0
- iqm/benchmarks/entanglement/ghz.py +802 -0
- iqm/benchmarks/logging_config.py +29 -0
- iqm/benchmarks/optimization/__init__.py +18 -0
- iqm/benchmarks/optimization/qscore.py +719 -0
- iqm/benchmarks/quantum_volume/__init__.py +21 -0
- iqm/benchmarks/quantum_volume/clops.py +726 -0
- iqm/benchmarks/quantum_volume/quantum_volume.py +854 -0
- iqm/benchmarks/randomized_benchmarking/__init__.py +18 -0
- iqm/benchmarks/randomized_benchmarking/clifford_1q.pkl +0 -0
- iqm/benchmarks/randomized_benchmarking/clifford_2q.pkl +0 -0
- iqm/benchmarks/randomized_benchmarking/clifford_rb/__init__.py +19 -0
- iqm/benchmarks/randomized_benchmarking/clifford_rb/clifford_rb.py +386 -0
- iqm/benchmarks/randomized_benchmarking/interleaved_rb/__init__.py +19 -0
- iqm/benchmarks/randomized_benchmarking/interleaved_rb/interleaved_rb.py +555 -0
- iqm/benchmarks/randomized_benchmarking/mirror_rb/__init__.py +19 -0
- iqm/benchmarks/randomized_benchmarking/mirror_rb/mirror_rb.py +810 -0
- iqm/benchmarks/randomized_benchmarking/multi_lmfit.py +86 -0
- iqm/benchmarks/randomized_benchmarking/randomized_benchmarking_common.py +892 -0
- iqm/benchmarks/readout_mitigation.py +290 -0
- iqm/benchmarks/utils.py +521 -0
- iqm_benchmarks-1.3.dist-info/LICENSE +205 -0
- iqm_benchmarks-1.3.dist-info/METADATA +190 -0
- iqm_benchmarks-1.3.dist-info/RECORD +42 -0
- iqm_benchmarks-1.3.dist-info/WHEEL +5 -0
- iqm_benchmarks-1.3.dist-info/top_level.txt +2 -0
- mGST/LICENSE +21 -0
- mGST/README.md +54 -0
- mGST/additional_fns.py +962 -0
- mGST/algorithm.py +733 -0
- mGST/compatibility.py +238 -0
- mGST/low_level_jit.py +694 -0
- mGST/optimization.py +349 -0
- mGST/qiskit_interface.py +282 -0
- mGST/reporting/figure_gen.py +334 -0
- mGST/reporting/reporting.py +710 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
# Copyright 2024 IQM Benchmarks developers
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Q-score benchmark
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import itertools
|
|
20
|
+
from time import strftime
|
|
21
|
+
from typing import Callable, Dict, List, Optional, Tuple, Type
|
|
22
|
+
|
|
23
|
+
from matplotlib.figure import Figure
|
|
24
|
+
import matplotlib.pyplot as plt
|
|
25
|
+
from networkx import Graph
|
|
26
|
+
import networkx as nx
|
|
27
|
+
import numpy as np
|
|
28
|
+
from qiskit import QuantumCircuit
|
|
29
|
+
from scipy.optimize import basinhopping, minimize
|
|
30
|
+
|
|
31
|
+
from iqm.benchmarks.benchmark import BenchmarkBase, BenchmarkConfigurationBase
|
|
32
|
+
from iqm.benchmarks.logging_config import qcvv_logger
|
|
33
|
+
from iqm.benchmarks.utils import perform_backend_transpilation, retrieve_all_counts, submit_execute, timeit
|
|
34
|
+
from iqm.qiskit_iqm.iqm_backend import IQMBackendBase
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class QScoreBenchmark(BenchmarkBase):
|
|
38
|
+
"""
|
|
39
|
+
Q-score estimates the size of combinatorial optimization problems a given number of qubits can execute with meaningful results.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, backend: IQMBackendBase, configuration: "QScoreConfiguration"):
|
|
43
|
+
"""Construct the QScoreBenchmark class.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
backend (IQMBackendBase): the backend to execute the benchmark on
|
|
47
|
+
configuration (QScoreConfiguration): the configuration of the benchmark
|
|
48
|
+
"""
|
|
49
|
+
super().__init__(backend, configuration)
|
|
50
|
+
|
|
51
|
+
self.num_instances = configuration.num_instances
|
|
52
|
+
self.num_qaoa_layers = configuration.num_qaoa_layers
|
|
53
|
+
self.min_num_nodes = configuration.min_num_nodes
|
|
54
|
+
self.max_num_nodes = configuration.max_num_nodes
|
|
55
|
+
self.use_virtual_node = configuration.use_virtual_node
|
|
56
|
+
self.use_classically_optimized_angles = configuration.use_classically_optimized_angles
|
|
57
|
+
self.choose_qubits_routine = configuration.choose_qubits_routine
|
|
58
|
+
self.qiskit_optim_level = configuration.qiskit_optim_level
|
|
59
|
+
self.optimize_sqg = configuration.optimize_sqg
|
|
60
|
+
self.timestamp = strftime("%Y%m%d-%H%M%S")
|
|
61
|
+
self.seed = configuration.seed
|
|
62
|
+
|
|
63
|
+
self.graph_physical: Graph
|
|
64
|
+
self.virtual_nodes: List[Tuple[int, int]]
|
|
65
|
+
self.node_to_qubit: Dict[int, int]
|
|
66
|
+
self.qubit_to_node: Dict[int, int]
|
|
67
|
+
|
|
68
|
+
if self.use_classically_optimized_angles and self.num_qaoa_layers > 1:
|
|
69
|
+
raise ValueError("If the `use_classically_optimized_angles` is chosen, the `num_qaoa_layers` must be 1.")
|
|
70
|
+
|
|
71
|
+
if self.use_virtual_node and self.num_qaoa_layers > 1:
|
|
72
|
+
raise ValueError("If the `use_virtual_node` is chosen, the `num_qaoa_layers` must be 1.")
|
|
73
|
+
|
|
74
|
+
if self.choose_qubits_routine == "custom":
|
|
75
|
+
self.custom_qubits_array = configuration.custom_qubits_array
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def name() -> str:
|
|
79
|
+
return "qscore"
|
|
80
|
+
|
|
81
|
+
def generate_maxcut_ansatz( # pylint: disable=too-many-branches
|
|
82
|
+
self,
|
|
83
|
+
graph: Graph,
|
|
84
|
+
theta: list[float],
|
|
85
|
+
) -> QuantumCircuit:
|
|
86
|
+
"""Generate an ansatz circuit for QAOA MaxCut, with measurements at the end.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
graph (networkx graph): the MaxCut problem graph
|
|
90
|
+
theta (list[float]): the variational parameters for QAOA, first gammas then betas
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
QuantumCircuit: the QAOA ansatz quantum circuit.
|
|
94
|
+
"""
|
|
95
|
+
gamma = theta[: self.num_qaoa_layers]
|
|
96
|
+
beta = theta[self.num_qaoa_layers :]
|
|
97
|
+
|
|
98
|
+
num_qubits = self.graph_physical.number_of_nodes()
|
|
99
|
+
|
|
100
|
+
if self.graph_physical.number_of_nodes() != graph.number_of_nodes():
|
|
101
|
+
num_qubits = self.graph_physical.number_of_nodes()
|
|
102
|
+
# re-label the nodes to be between 0 and _num_qubits
|
|
103
|
+
self.node_to_qubit = {node: qubit for qubit, node in enumerate(list(self.graph_physical.nodes))}
|
|
104
|
+
self.qubit_to_node = dict(enumerate(list(self.graph_physical.nodes)))
|
|
105
|
+
else:
|
|
106
|
+
num_qubits = graph.number_of_nodes()
|
|
107
|
+
self.node_to_qubit = {node: node for node in list(self.graph_physical.nodes)} # no relabeling
|
|
108
|
+
self.qubit_to_node = self.node_to_qubit
|
|
109
|
+
|
|
110
|
+
# in case the graph is trivial: return empty circuit
|
|
111
|
+
if num_qubits == 0:
|
|
112
|
+
return QuantumCircuit(1)
|
|
113
|
+
|
|
114
|
+
qaoa_qc = QuantumCircuit(num_qubits)
|
|
115
|
+
for i in range(0, num_qubits):
|
|
116
|
+
qaoa_qc.h(i)
|
|
117
|
+
for layer in range(self.num_qaoa_layers):
|
|
118
|
+
for edge in self.graph_physical.edges():
|
|
119
|
+
i = self.node_to_qubit[edge[0]]
|
|
120
|
+
j = self.node_to_qubit[edge[1]]
|
|
121
|
+
qaoa_qc.rzz(2 * gamma[layer], i, j)
|
|
122
|
+
|
|
123
|
+
# include edges of the virtual node as rz terms
|
|
124
|
+
for vn in self.virtual_nodes:
|
|
125
|
+
for edge in graph.edges(vn[0]):
|
|
126
|
+
# exclude edges between virtual nodes
|
|
127
|
+
edges_between_virtual_nodes = list(itertools.combinations([i[0] for i in self.virtual_nodes], 2))
|
|
128
|
+
if set(edge) not in list(map(set, edges_between_virtual_nodes)):
|
|
129
|
+
# The value of the fixed node defines the sign of the rz gate
|
|
130
|
+
sign = 1.0
|
|
131
|
+
if vn[1] == 1:
|
|
132
|
+
sign = -1.0
|
|
133
|
+
qaoa_qc.rz(sign * 2.0 * gamma[layer], self.node_to_qubit[edge[1]])
|
|
134
|
+
|
|
135
|
+
for i in range(0, num_qubits):
|
|
136
|
+
qaoa_qc.rx(2 * beta[layer], i)
|
|
137
|
+
qaoa_qc.measure_all()
|
|
138
|
+
|
|
139
|
+
return qaoa_qc
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def cost_function(x: str, graph: Graph) -> int:
|
|
143
|
+
"""Returns the number of cut edges in a graph (with minus sign).
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
x (str): solution bitstring.
|
|
147
|
+
graph (networkx graph): the MaxCut problem graph.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
obj (float): number of cut edges multiplied by -1.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
obj = 0
|
|
154
|
+
for i, j in graph.edges():
|
|
155
|
+
if x[i] != x[j]:
|
|
156
|
+
obj += 1
|
|
157
|
+
|
|
158
|
+
return -1 * obj
|
|
159
|
+
|
|
160
|
+
def compute_expectation_value(self, counts: Dict[str, int], graph: Graph) -> float:
|
|
161
|
+
"""Computes expectation value based on measurement results.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
counts (Dict[str, int]): key as bitstring, val as count
|
|
165
|
+
graph (networkx) graph: the MaxCut problem graph
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
avg (float): expectation value of the cut edges for number of counts
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
avg = 0
|
|
172
|
+
sum_count = 0
|
|
173
|
+
for bitstring_aux, count in counts.items():
|
|
174
|
+
bitstring_aux_list = list(bitstring_aux)[::-1] # go from qiskit endianness to networkx endianness
|
|
175
|
+
|
|
176
|
+
# map the qubits back to nodes
|
|
177
|
+
bitstring = [""] * (len(bitstring_aux_list) + len(self.virtual_nodes))
|
|
178
|
+
for qubit, node in self.qubit_to_node.items():
|
|
179
|
+
bitstring[node] = bitstring_aux_list[qubit]
|
|
180
|
+
|
|
181
|
+
# insert virtual node(s) to bitstring
|
|
182
|
+
for virtual_node in self.virtual_nodes:
|
|
183
|
+
if virtual_node[0] is not None:
|
|
184
|
+
bitstring[virtual_node[0]] = str(virtual_node[1])
|
|
185
|
+
|
|
186
|
+
obj = self.cost_function("".join(bitstring), graph)
|
|
187
|
+
avg += obj * count
|
|
188
|
+
sum_count += count
|
|
189
|
+
|
|
190
|
+
return avg / sum_count
|
|
191
|
+
|
|
192
|
+
def create_objective_function(self, graph: Graph, qubit_set: List[int]) -> Callable:
|
|
193
|
+
"""
|
|
194
|
+
Creates a function that maps the parameters to the parametrized circuit,
|
|
195
|
+
runs it and computes the expectation value.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
graph (networkx graph): the MaxCut problem graph.
|
|
199
|
+
qubit_set (List[int]): indices of the used qubits.
|
|
200
|
+
Returns:
|
|
201
|
+
callable: function that gives expectation value of the cut edges from counts sampled from the ansatz
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def objective_function(theta):
|
|
205
|
+
qc = self.generate_maxcut_ansatz(graph, theta)
|
|
206
|
+
|
|
207
|
+
if len(qc.count_ops()) == 0:
|
|
208
|
+
counts = {"": 1.0} # to handle the case of physical graph with no edges
|
|
209
|
+
|
|
210
|
+
else:
|
|
211
|
+
coupling_map = self.backend.coupling_map.reduce(qubit_set)
|
|
212
|
+
transpiled_qc_list, _ = perform_backend_transpilation(
|
|
213
|
+
[qc],
|
|
214
|
+
backend=self.backend,
|
|
215
|
+
qubits=qubit_set,
|
|
216
|
+
coupling_map=coupling_map,
|
|
217
|
+
qiskit_optim_level=self.qiskit_optim_level,
|
|
218
|
+
optimize_sqg=self.optimize_sqg,
|
|
219
|
+
routing_method=self.routing_method,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
sorted_transpiled_qc_list = {tuple(qubit_set): transpiled_qc_list}
|
|
223
|
+
# Execute on the backend
|
|
224
|
+
jobs, _ = submit_execute(
|
|
225
|
+
sorted_transpiled_qc_list,
|
|
226
|
+
self.backend,
|
|
227
|
+
self.shots,
|
|
228
|
+
self.calset_id,
|
|
229
|
+
max_gates_per_batch=self.max_gates_per_batch,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
counts = retrieve_all_counts(jobs)[0][0]
|
|
233
|
+
|
|
234
|
+
return self.compute_expectation_value(counts, graph)
|
|
235
|
+
|
|
236
|
+
return objective_function
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def calculate_optimal_angles_for_QAOA_p1(graph: Graph) -> List[float]:
|
|
240
|
+
"""
|
|
241
|
+
Calculates the optimal angles for single layer QAOA MaxCut ansatz.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
graph (networkx graph): the MaxCut problem graph.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
List[float]: optimal angles gamma and beta.
|
|
248
|
+
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
def get_Zij_maxcut_p1(edge_ij, gamma, beta):
|
|
252
|
+
"""
|
|
253
|
+
Calculates <p1_QAOA | Z_i Z_j | p1_QAOA>, assuming ij is edge of G.
|
|
254
|
+
"""
|
|
255
|
+
i, j = edge_ij
|
|
256
|
+
di = graph.degree[i]
|
|
257
|
+
dj = graph.degree[j]
|
|
258
|
+
|
|
259
|
+
first = np.cos(2 * gamma) ** (di - 1) + np.cos(2 * gamma) ** (dj - 1)
|
|
260
|
+
first *= 0.5 * np.sin(4 * beta) * np.sin(2 * gamma)
|
|
261
|
+
|
|
262
|
+
node_list = list(graph.nodes).copy()
|
|
263
|
+
node_list.remove(i)
|
|
264
|
+
node_list.remove(j)
|
|
265
|
+
f1 = 1
|
|
266
|
+
f2 = 1
|
|
267
|
+
for k in node_list:
|
|
268
|
+
if graph.has_edge(i, k) and graph.has_edge(j, k): # ijk is triangle
|
|
269
|
+
f1 *= np.cos(4 * gamma)
|
|
270
|
+
elif graph.has_edge(i, k) or graph.has_edge(j, k): # ijk is no triangle
|
|
271
|
+
f1 *= np.cos(2 * gamma)
|
|
272
|
+
f2 *= np.cos(2 * gamma)
|
|
273
|
+
second = 0.5 * np.sin(2 * beta) ** 2 * (f1 - f2)
|
|
274
|
+
return first - second
|
|
275
|
+
|
|
276
|
+
def get_expected_zz_edgedensity(x):
|
|
277
|
+
gamma = x[0]
|
|
278
|
+
beta = x[1]
|
|
279
|
+
# pylint: disable=consider-using-generator
|
|
280
|
+
return sum([get_Zij_maxcut_p1(edge, gamma, beta) for edge in graph.edges]) / graph.number_of_edges()
|
|
281
|
+
|
|
282
|
+
bounds = [(0.0, np.pi / 2), (-np.pi / 4, 0.0)]
|
|
283
|
+
x_init = [0.15, -0.28]
|
|
284
|
+
|
|
285
|
+
minimizer_kwargs = {"method": "L-BFGS-B", "bounds": bounds}
|
|
286
|
+
res = basinhopping(get_expected_zz_edgedensity, x_init, minimizer_kwargs=minimizer_kwargs, niter=10, T=2)
|
|
287
|
+
|
|
288
|
+
return res.x
|
|
289
|
+
|
|
290
|
+
def run_QAOA(self, graph: Graph, qubit_set: List[int]) -> float:
|
|
291
|
+
"""
|
|
292
|
+
Solves the cut size of MaxCut for a graph using QAOA.
|
|
293
|
+
The result is average value sampled from the optimized ansatz.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
graph (networkx graph): the MaxCut problem graph.
|
|
297
|
+
qubit_set (List[int]): indices of the used qubits.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
float: the expectation value of the maximum cut size.
|
|
301
|
+
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
objective_function = self.create_objective_function(graph, qubit_set)
|
|
305
|
+
|
|
306
|
+
if self.use_classically_optimized_angles:
|
|
307
|
+
if self.graph_physical.number_of_edges() != 0:
|
|
308
|
+
opt_angles = self.calculate_optimal_angles_for_QAOA_p1(self.graph_physical)
|
|
309
|
+
else:
|
|
310
|
+
opt_angles = [1.0, 1.0]
|
|
311
|
+
res = minimize(objective_function, opt_angles, method="COBYLA", tol=1e-5, options={"maxiter": 0})
|
|
312
|
+
else:
|
|
313
|
+
# Good initial angles from from Wurtz et.al. "The fixed angle conjecture for QAOA on regular MaxCut graphs." arXiv preprint arXiv:2107.00677 (2021).
|
|
314
|
+
OPTIMAL_INITIAL_ANGLES = {
|
|
315
|
+
"1": [-0.616, 0.393 / 2],
|
|
316
|
+
"2": [-0.488, 0.898 / 2, 0.555 / 2, 0.293 / 2],
|
|
317
|
+
"3": [-0.422, 0.798 / 2, 0.937 / 2, 0.609 / 2, 0.459 / 2, 0.235 / 2],
|
|
318
|
+
"4": [-0.409, 0.781 / 2, 0.988 / 2, 1.156 / 2, 0.600 / 2, 0.434 / 2, 0.297 / 2, 0.159 / 2],
|
|
319
|
+
"5": [-0.36, -0.707, -0.823, -1.005, -1.154, 0.632 / 2, 0.523 / 2, 0.390 / 2, 0.275 / 2, 0.149 / 2],
|
|
320
|
+
"6": [
|
|
321
|
+
-0.331,
|
|
322
|
+
-0.645,
|
|
323
|
+
-0.731,
|
|
324
|
+
-0.837,
|
|
325
|
+
-1.009,
|
|
326
|
+
-1.126,
|
|
327
|
+
0.636 / 2,
|
|
328
|
+
0.535 / 2,
|
|
329
|
+
0.463 / 2,
|
|
330
|
+
0.360 / 2,
|
|
331
|
+
0.259 / 2,
|
|
332
|
+
0.139 / 2,
|
|
333
|
+
],
|
|
334
|
+
"7": [
|
|
335
|
+
-0.310,
|
|
336
|
+
-0.618,
|
|
337
|
+
-0.690,
|
|
338
|
+
-0.751,
|
|
339
|
+
-0.859,
|
|
340
|
+
-1.020,
|
|
341
|
+
-1.122,
|
|
342
|
+
0.648 / 2,
|
|
343
|
+
0.554 / 2,
|
|
344
|
+
0.490 / 2,
|
|
345
|
+
0.445 / 2,
|
|
346
|
+
0.341 / 2,
|
|
347
|
+
0.244 / 2,
|
|
348
|
+
0.131 / 2,
|
|
349
|
+
],
|
|
350
|
+
"8": [
|
|
351
|
+
-0.295,
|
|
352
|
+
-0.587,
|
|
353
|
+
-0.654,
|
|
354
|
+
-0.708,
|
|
355
|
+
-0.765,
|
|
356
|
+
-0.864,
|
|
357
|
+
-1.026,
|
|
358
|
+
-1.116,
|
|
359
|
+
0.649 / 2,
|
|
360
|
+
0.555 / 2,
|
|
361
|
+
0.500 / 2,
|
|
362
|
+
0.469 / 2,
|
|
363
|
+
0.420 / 2,
|
|
364
|
+
0.319 / 2,
|
|
365
|
+
0.231 / 2,
|
|
366
|
+
0.123 / 2,
|
|
367
|
+
],
|
|
368
|
+
"9": [
|
|
369
|
+
-0.279,
|
|
370
|
+
-0.566,
|
|
371
|
+
-0.631,
|
|
372
|
+
-0.679,
|
|
373
|
+
-0.726,
|
|
374
|
+
-0.768,
|
|
375
|
+
-0.875,
|
|
376
|
+
-1.037,
|
|
377
|
+
-1.118,
|
|
378
|
+
0.654 / 2,
|
|
379
|
+
0.562 / 2,
|
|
380
|
+
0.509 / 2,
|
|
381
|
+
0.487 / 2,
|
|
382
|
+
0.451 / 2,
|
|
383
|
+
0.403 / 2,
|
|
384
|
+
0.305 / 2,
|
|
385
|
+
0.220 / 2,
|
|
386
|
+
0.117 / 2,
|
|
387
|
+
],
|
|
388
|
+
"10": [
|
|
389
|
+
-0.267,
|
|
390
|
+
-0.545,
|
|
391
|
+
-0.610,
|
|
392
|
+
-0.656,
|
|
393
|
+
-0.696,
|
|
394
|
+
-0.729,
|
|
395
|
+
-0.774,
|
|
396
|
+
-0.882,
|
|
397
|
+
-1.044,
|
|
398
|
+
-1.115,
|
|
399
|
+
0.656 / 2,
|
|
400
|
+
0.563 / 2,
|
|
401
|
+
0.514 / 2,
|
|
402
|
+
0.496 / 2,
|
|
403
|
+
0.496 / 2,
|
|
404
|
+
0.436 / 2,
|
|
405
|
+
0.388 / 2,
|
|
406
|
+
0.291 / 2,
|
|
407
|
+
0.211 / 2,
|
|
408
|
+
0.112 / 2,
|
|
409
|
+
],
|
|
410
|
+
"11": [
|
|
411
|
+
-0.257,
|
|
412
|
+
-0.528,
|
|
413
|
+
-0.592,
|
|
414
|
+
-0.640,
|
|
415
|
+
-0.677,
|
|
416
|
+
-0.702,
|
|
417
|
+
-0.737,
|
|
418
|
+
-0.775,
|
|
419
|
+
-0.884,
|
|
420
|
+
-1.047,
|
|
421
|
+
-1.115,
|
|
422
|
+
0.656 / 2,
|
|
423
|
+
0.563 / 2,
|
|
424
|
+
0.516 / 2,
|
|
425
|
+
0.504 / 2,
|
|
426
|
+
0.482 / 2,
|
|
427
|
+
0.456 / 2,
|
|
428
|
+
0.421 / 2,
|
|
429
|
+
0.371 / 2,
|
|
430
|
+
0.276 / 2,
|
|
431
|
+
0.201 / 2,
|
|
432
|
+
0.107 / 2,
|
|
433
|
+
],
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
theta = OPTIMAL_INITIAL_ANGLES[str(self.num_qaoa_layers)]
|
|
437
|
+
bounds = [(-np.pi, np.pi)] * self.num_qaoa_layers + [(0.0, np.pi)] * self.num_qaoa_layers
|
|
438
|
+
|
|
439
|
+
res = minimize(
|
|
440
|
+
objective_function,
|
|
441
|
+
theta,
|
|
442
|
+
bounds=bounds,
|
|
443
|
+
method="COBYLA",
|
|
444
|
+
tol=1e-5,
|
|
445
|
+
options={"maxiter": 300},
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
return -res.fun
|
|
449
|
+
|
|
450
|
+
@staticmethod
|
|
451
|
+
def is_successful(
|
|
452
|
+
approximation_ratio: float,
|
|
453
|
+
) -> bool:
|
|
454
|
+
"""Check whether a Q-score benchmark returned approximation ratio above beta*, therefore being successful.
|
|
455
|
+
|
|
456
|
+
This condition checks that the mean approximation ratio is above the beta* = 0.2 threshold.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
approximation_ratio (float): the mean approximation ratio of all problem graphs
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
bool: whether the Q-score benchmark was successful
|
|
463
|
+
"""
|
|
464
|
+
return bool(approximation_ratio > 0.2)
|
|
465
|
+
|
|
466
|
+
@staticmethod
|
|
467
|
+
def choose_qubits_naive(num_qubits: int) -> list[int]:
|
|
468
|
+
"""Choose the qubits to execute the circuits on, sequentially starting at qubit 0.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
num_qubits (int): the number of qubits to choose.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
list[int]: the list of qubits to execute the circuits on.
|
|
475
|
+
"""
|
|
476
|
+
if num_qubits == 2:
|
|
477
|
+
return [0, 2]
|
|
478
|
+
|
|
479
|
+
return list(range(num_qubits))
|
|
480
|
+
|
|
481
|
+
def choose_qubits_custom(self, num_qubits: int) -> list[int]:
|
|
482
|
+
"""Choose the qubits to execute the circuits on, according to elements in custom_qubits_array matching num_qubits number of qubits
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
num_qubits (int): the number of qubits to choose
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
list[int]: the list of qubits to execute the circuits on
|
|
489
|
+
"""
|
|
490
|
+
if self.custom_qubits_array is None:
|
|
491
|
+
raise ValueError(
|
|
492
|
+
"If the `choose_qubits_custom` routine is chosen, a `custom_qubits_array` must be specified in `QScoreConfiguration`."
|
|
493
|
+
)
|
|
494
|
+
selected_qubits = [qubit_layout for qubit_layout in self.custom_qubits_array if len(qubit_layout) == num_qubits]
|
|
495
|
+
# User may input more than one num_qubits layouts
|
|
496
|
+
if len(selected_qubits) > 1:
|
|
497
|
+
chosen_qubits = selected_qubits[0]
|
|
498
|
+
self.custom_qubits_array.remove(chosen_qubits)
|
|
499
|
+
# The execute_single_benchmark call must be looped through a COPY of custom_qubits_array
|
|
500
|
+
else:
|
|
501
|
+
chosen_qubits = selected_qubits[0]
|
|
502
|
+
return chosen_qubits
|
|
503
|
+
|
|
504
|
+
def plot_approximation_ratios(
|
|
505
|
+
self, list_of_num_nodes: list[int], list_of_cut_sizes: list[list[float]]
|
|
506
|
+
) -> tuple[str, Figure]:
|
|
507
|
+
"""Generate the figure of approximation ratios vs number of nodes,
|
|
508
|
+
including standard deviation and the acceptance threshold.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
list_of_num_nodes (list[int]): list of problem graph sizes.
|
|
512
|
+
list_of_cut_sizes (list[list[float]]): the list of lists of maximum average cut sizes of problem graph instances for each problem size.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
str: the name of the figure.
|
|
516
|
+
Figure: the figure.
|
|
517
|
+
"""
|
|
518
|
+
|
|
519
|
+
LAMBDA = 0.178
|
|
520
|
+
|
|
521
|
+
approximation_ratio_lists = {
|
|
522
|
+
num_nodes: (np.array(cut_sizes) - num_nodes * (num_nodes - 1) / 8) / (LAMBDA * num_nodes ** (3 / 2))
|
|
523
|
+
for num_nodes, cut_sizes in zip(list_of_num_nodes, list_of_cut_sizes)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
avg_approximation_ratios = {
|
|
527
|
+
num_nodes: np.mean(approximation_ratios)
|
|
528
|
+
for num_nodes, approximation_ratios in approximation_ratio_lists.items()
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
std_of_approximation_ratios = {
|
|
532
|
+
num_nodes: np.std(approximation_ratios) / np.sqrt(len(approximation_ratios) - 1)
|
|
533
|
+
for num_nodes, approximation_ratios in approximation_ratio_lists.items()
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
nodes = list(avg_approximation_ratios.keys())
|
|
537
|
+
ratios = list(avg_approximation_ratios.values())
|
|
538
|
+
std = list(std_of_approximation_ratios.values())
|
|
539
|
+
|
|
540
|
+
fig = plt.figure()
|
|
541
|
+
ax = plt.axes()
|
|
542
|
+
|
|
543
|
+
plt.axhline(0.2, color="red", linestyle="dashed", label="Threshold")
|
|
544
|
+
plt.errorbar(
|
|
545
|
+
nodes, ratios, yerr=std, fmt="-o", capsize=10, markersize=8, color="#759DEB", label="Approximation ratio"
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
ax.set_ylabel(r"Q-score ratio $\beta(n)$")
|
|
549
|
+
ax.set_xlabel("Number of nodes $(n)$")
|
|
550
|
+
plt.xticks(range(min(nodes), max(nodes) + 1))
|
|
551
|
+
plt.legend(loc="lower right")
|
|
552
|
+
plt.grid(True)
|
|
553
|
+
|
|
554
|
+
if self.use_virtual_node and self.use_classically_optimized_angles:
|
|
555
|
+
title = f"Q-score, {self.num_instances} instances, with virtual node and classically optimized angles\nBackend: {self.backend.name} / {self.timestamp}"
|
|
556
|
+
elif self.use_virtual_node and not self.use_classically_optimized_angles:
|
|
557
|
+
title = f"Q-score, {self.num_instances} instances, with virtual node \nBackend: {self.backend.name} / {self.timestamp}"
|
|
558
|
+
elif not self.use_virtual_node and self.use_classically_optimized_angles:
|
|
559
|
+
title = f"Q-score, {self.num_instances} instances, with classically optimized angles\nBackend: {self.backend.name} / {self.timestamp}"
|
|
560
|
+
else:
|
|
561
|
+
title = f"Q-score, {self.num_instances} instances \nBackend: {self.backend.name} / {self.timestamp}"
|
|
562
|
+
|
|
563
|
+
plt.title(
|
|
564
|
+
title,
|
|
565
|
+
fontsize=9,
|
|
566
|
+
)
|
|
567
|
+
fig_name = f"{max(nodes)}_nodes_{self.num_instances}_instances.png"
|
|
568
|
+
|
|
569
|
+
# Show plot if verbose is True
|
|
570
|
+
plt.gcf().set_dpi(250)
|
|
571
|
+
plt.show()
|
|
572
|
+
|
|
573
|
+
plt.close()
|
|
574
|
+
|
|
575
|
+
return fig_name, fig
|
|
576
|
+
|
|
577
|
+
def execute_single_benchmark(
|
|
578
|
+
self,
|
|
579
|
+
num_nodes: int,
|
|
580
|
+
) -> tuple[bool, float, list[float], list[int]]:
|
|
581
|
+
"""Execute a single benchmark, for a given number of qubits.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
num_nodes (int): number of nodes in the MaxCut problem graphs.
|
|
585
|
+
|
|
586
|
+
Returns:
|
|
587
|
+
bool: whether the benchmark was successful.
|
|
588
|
+
float: approximation_ratio.
|
|
589
|
+
list[float]: the list of maximum average cut sizes of problem graph instances.
|
|
590
|
+
list[int]: the set of qubits the Q-score benchmark was executed on.
|
|
591
|
+
"""
|
|
592
|
+
|
|
593
|
+
cut_sizes: list[float] = []
|
|
594
|
+
seed = self.seed
|
|
595
|
+
|
|
596
|
+
for i in range(self.num_instances):
|
|
597
|
+
graph = nx.generators.erdos_renyi_graph(num_nodes, 0.5, seed=seed)
|
|
598
|
+
print(f"graph: {graph}")
|
|
599
|
+
self.graph_physical = graph.copy()
|
|
600
|
+
self.virtual_nodes = []
|
|
601
|
+
if self.use_virtual_node:
|
|
602
|
+
virtual_node, _ = max(
|
|
603
|
+
graph.degree(), key=lambda x: x[1]
|
|
604
|
+
) # choose the virtual node as the most connected node
|
|
605
|
+
self.virtual_nodes.append(
|
|
606
|
+
(virtual_node, 1)
|
|
607
|
+
) # the second element of the tuple is the value assigned to the virtual node
|
|
608
|
+
self.graph_physical.remove_node(self.virtual_nodes[0][0])
|
|
609
|
+
# See if there are any non-connected nodes if so, remove them also
|
|
610
|
+
# and set them to be opposite value to the possible original virtual node
|
|
611
|
+
for node in self.graph_physical.nodes():
|
|
612
|
+
if self.graph_physical.degree(node) == 0:
|
|
613
|
+
self.virtual_nodes.append((node, 0))
|
|
614
|
+
for vn in self.virtual_nodes:
|
|
615
|
+
if self.graph_physical.has_node(vn[0]):
|
|
616
|
+
self.graph_physical.remove_node(vn[0])
|
|
617
|
+
|
|
618
|
+
# Graph with no edges has cut size = 0
|
|
619
|
+
if graph.number_of_edges() == 0:
|
|
620
|
+
cut_sizes.append(0)
|
|
621
|
+
seed += 1
|
|
622
|
+
qcvv_logger.info(f"Graph {i+1}/{self.num_instances} had no edges: cut size = 0.")
|
|
623
|
+
continue
|
|
624
|
+
|
|
625
|
+
# Choose the qubit layout
|
|
626
|
+
qubit_set = []
|
|
627
|
+
if self.choose_qubits_routine.lower() == "naive":
|
|
628
|
+
qubit_set = self.choose_qubits_naive(num_nodes)
|
|
629
|
+
elif self.choose_qubits_routine.lower() == "custom" or self.choose_qubits_routine.lower() == "mapomatic":
|
|
630
|
+
qubit_set = self.choose_qubits_custom(num_nodes)
|
|
631
|
+
else:
|
|
632
|
+
raise ValueError('choose_qubits_routine must either be "naive" or "custom".')
|
|
633
|
+
|
|
634
|
+
# Solve the maximum cut size with QAOA
|
|
635
|
+
cut_sizes.append(self.run_QAOA(graph, qubit_set))
|
|
636
|
+
seed += 1
|
|
637
|
+
qcvv_logger.info(f"Solved the MaxCut on graph {i+1}/{self.num_instances}.")
|
|
638
|
+
|
|
639
|
+
average_cut_size = np.mean(cut_sizes) - num_nodes * (num_nodes - 1) / 8
|
|
640
|
+
average_best_cut_size = 0.178 * pow(num_nodes, 3 / 2)
|
|
641
|
+
approximation_ratio = float(average_cut_size / average_best_cut_size)
|
|
642
|
+
|
|
643
|
+
self.raw_data[num_nodes] = {
|
|
644
|
+
"qubit_set": qubit_set,
|
|
645
|
+
"cut_sizes": cut_sizes,
|
|
646
|
+
}
|
|
647
|
+
self.results[num_nodes] = {
|
|
648
|
+
"qubit_set": qubit_set,
|
|
649
|
+
"is_successful": str(self.is_successful(approximation_ratio)),
|
|
650
|
+
"approximation_ratio": approximation_ratio,
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
# Return whether the single Q-score Benchmark was successful and its mean approximation ratio
|
|
654
|
+
# and cut sizes for all instances.
|
|
655
|
+
return self.is_successful(approximation_ratio), approximation_ratio, cut_sizes, qubit_set
|
|
656
|
+
|
|
657
|
+
@timeit
|
|
658
|
+
def execute_full_benchmark(self) -> tuple[int, list[float], list[list[float]]]:
|
|
659
|
+
"""Execute the full benchmark, starting with self.min_num_nodes nodes up to failure.
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
int: the Q-score of the device.
|
|
663
|
+
list[float]: the list of approximation rations over problem graph instances for each problem size.
|
|
664
|
+
list[list[float]]: the list of lists of maximum average cut sizes of problem graph instances for each problem size.
|
|
665
|
+
"""
|
|
666
|
+
qscore = 0
|
|
667
|
+
|
|
668
|
+
if self.max_num_nodes is None:
|
|
669
|
+
if self.use_virtual_node:
|
|
670
|
+
max_num_nodes = self.backend.num_qubits + 1
|
|
671
|
+
else:
|
|
672
|
+
max_num_nodes = self.backend.num_qubits
|
|
673
|
+
else:
|
|
674
|
+
max_num_nodes = self.max_num_nodes
|
|
675
|
+
|
|
676
|
+
approximation_ratios = []
|
|
677
|
+
list_of_cut_sizes = []
|
|
678
|
+
|
|
679
|
+
for num_nodes in range(self.min_num_nodes, max_num_nodes + 1):
|
|
680
|
+
qcvv_logger.info(f"Executing on {self.num_instances} random graphs with {num_nodes} nodes.")
|
|
681
|
+
is_successful, approximation_ratio, cut_sizes = self.execute_single_benchmark(num_nodes)[0:3]
|
|
682
|
+
approximation_ratios.append(approximation_ratio)
|
|
683
|
+
|
|
684
|
+
list_of_cut_sizes.append(cut_sizes)
|
|
685
|
+
if is_successful:
|
|
686
|
+
qcvv_logger.info(
|
|
687
|
+
f"Q-Score = {num_nodes} passed with:\nApproximation ratio (Beta): {approximation_ratio:.4f}; Avg MaxCut size: {np.mean(cut_sizes):.4f}"
|
|
688
|
+
)
|
|
689
|
+
qscore = num_nodes
|
|
690
|
+
continue
|
|
691
|
+
|
|
692
|
+
qcvv_logger.info(
|
|
693
|
+
f"Q-Score = {num_nodes} failed with \napproximation ratio (Beta): {approximation_ratio:.4f} < 0.2; Avg MaxCut size: {np.mean(cut_sizes):.4f}"
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
self.results["qscore"] = qscore
|
|
697
|
+
fig_name, fig = self.plot_approximation_ratios(
|
|
698
|
+
list(range(self.min_num_nodes, max_num_nodes + 1)), list_of_cut_sizes
|
|
699
|
+
)
|
|
700
|
+
self.figures[fig_name] = fig
|
|
701
|
+
return num_nodes, approximation_ratios, list_of_cut_sizes
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
class QScoreConfiguration(BenchmarkConfigurationBase):
|
|
705
|
+
"""Q-score configuration."""
|
|
706
|
+
|
|
707
|
+
benchmark: Type[BenchmarkBase] = QScoreBenchmark
|
|
708
|
+
num_instances: int
|
|
709
|
+
num_qaoa_layers: int = 1
|
|
710
|
+
min_num_nodes: int = 2
|
|
711
|
+
max_num_nodes: Optional[int] = None
|
|
712
|
+
use_virtual_node: bool = True
|
|
713
|
+
use_classically_optimized_angles: bool = True
|
|
714
|
+
choose_qubits_routine: str = "naive"
|
|
715
|
+
min_num_qubits: int = 2 # If choose_qubits_routine is "naive"
|
|
716
|
+
custom_qubits_array: Optional[list[list[int]]] = None
|
|
717
|
+
qiskit_optim_level: int = 3
|
|
718
|
+
optimize_sqg: bool = True
|
|
719
|
+
seed: int = 1
|