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.

Files changed (42) hide show
  1. iqm/benchmarks/__init__.py +31 -0
  2. iqm/benchmarks/benchmark.py +109 -0
  3. iqm/benchmarks/benchmark_definition.py +264 -0
  4. iqm/benchmarks/benchmark_experiment.py +163 -0
  5. iqm/benchmarks/compressive_gst/__init__.py +20 -0
  6. iqm/benchmarks/compressive_gst/compressive_gst.py +1029 -0
  7. iqm/benchmarks/entanglement/__init__.py +18 -0
  8. iqm/benchmarks/entanglement/ghz.py +802 -0
  9. iqm/benchmarks/logging_config.py +29 -0
  10. iqm/benchmarks/optimization/__init__.py +18 -0
  11. iqm/benchmarks/optimization/qscore.py +719 -0
  12. iqm/benchmarks/quantum_volume/__init__.py +21 -0
  13. iqm/benchmarks/quantum_volume/clops.py +726 -0
  14. iqm/benchmarks/quantum_volume/quantum_volume.py +854 -0
  15. iqm/benchmarks/randomized_benchmarking/__init__.py +18 -0
  16. iqm/benchmarks/randomized_benchmarking/clifford_1q.pkl +0 -0
  17. iqm/benchmarks/randomized_benchmarking/clifford_2q.pkl +0 -0
  18. iqm/benchmarks/randomized_benchmarking/clifford_rb/__init__.py +19 -0
  19. iqm/benchmarks/randomized_benchmarking/clifford_rb/clifford_rb.py +386 -0
  20. iqm/benchmarks/randomized_benchmarking/interleaved_rb/__init__.py +19 -0
  21. iqm/benchmarks/randomized_benchmarking/interleaved_rb/interleaved_rb.py +555 -0
  22. iqm/benchmarks/randomized_benchmarking/mirror_rb/__init__.py +19 -0
  23. iqm/benchmarks/randomized_benchmarking/mirror_rb/mirror_rb.py +810 -0
  24. iqm/benchmarks/randomized_benchmarking/multi_lmfit.py +86 -0
  25. iqm/benchmarks/randomized_benchmarking/randomized_benchmarking_common.py +892 -0
  26. iqm/benchmarks/readout_mitigation.py +290 -0
  27. iqm/benchmarks/utils.py +521 -0
  28. iqm_benchmarks-1.3.dist-info/LICENSE +205 -0
  29. iqm_benchmarks-1.3.dist-info/METADATA +190 -0
  30. iqm_benchmarks-1.3.dist-info/RECORD +42 -0
  31. iqm_benchmarks-1.3.dist-info/WHEEL +5 -0
  32. iqm_benchmarks-1.3.dist-info/top_level.txt +2 -0
  33. mGST/LICENSE +21 -0
  34. mGST/README.md +54 -0
  35. mGST/additional_fns.py +962 -0
  36. mGST/algorithm.py +733 -0
  37. mGST/compatibility.py +238 -0
  38. mGST/low_level_jit.py +694 -0
  39. mGST/optimization.py +349 -0
  40. mGST/qiskit_interface.py +282 -0
  41. mGST/reporting/figure_gen.py +334 -0
  42. 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