iqm-benchmarks 2.3__py3-none-any.whl → 2.5__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.

@@ -1,53 +1,571 @@
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
1
  """
16
- Q-score benchmark
2
+ Qscore benchmark
17
3
  """
18
4
 
19
5
  import itertools
20
6
  import logging
21
7
  from time import strftime
22
- from typing import Callable, Dict, List, Literal, Optional, Tuple, Type
8
+ from typing import Callable, Dict, List, Literal, Optional, Sequence, Tuple, Type, cast
23
9
 
24
10
  from matplotlib.figure import Figure
25
11
  import matplotlib.pyplot as plt
26
12
  from networkx import Graph
27
13
  import networkx as nx
28
14
  import numpy as np
15
+ from qiskit import QuantumCircuit
29
16
  from scipy.optimize import basinhopping, minimize
30
-
31
- from iqm.benchmarks.benchmark import BenchmarkBase, BenchmarkConfigurationBase
17
+ import xarray as xr
18
+
19
+ from iqm.benchmarks.benchmark import BenchmarkConfigurationBase
20
+ from iqm.benchmarks.benchmark_definition import (
21
+ Benchmark,
22
+ BenchmarkAnalysisResult,
23
+ BenchmarkObservation,
24
+ BenchmarkObservationIdentifier,
25
+ BenchmarkRunResult,
26
+ add_counts_to_dataset,
27
+ )
28
+ from iqm.benchmarks.circuit_containers import BenchmarkCircuit, CircuitGroup, Circuits
32
29
  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 import IQMCircuit as QuantumCircuit
30
+ from iqm.benchmarks.utils import ( # execute_with_dd,
31
+ perform_backend_transpilation,
32
+ retrieve_all_counts,
33
+ submit_execute,
34
+ xrvariable_to_counts,
35
+ )
35
36
  from iqm.qiskit_iqm.iqm_backend import IQMBackendBase
36
37
 
37
38
 
38
- class QScoreBenchmark(BenchmarkBase):
39
+ def calculate_optimal_angles_for_QAOA_p1(graph: Graph) -> List[float]:
40
+ """Calculates the optimal angles for single layer QAOA MaxCut ansatz.
41
+
42
+ Args:
43
+ graph (networkx graph): the MaxCut problem graph.
44
+
45
+ Returns:
46
+ List[float]: optimal angles gamma and beta.
47
+
48
+ """
49
+
50
+ def get_Zij_maxcut_p1(edge_ij, gamma, beta):
51
+ """
52
+ Calculates <p1_QAOA | Z_i Z_j | p1_QAOA>, assuming ij is edge of G.
53
+ """
54
+ i, j = edge_ij
55
+ di = graph.degree[i]
56
+ dj = graph.degree[j]
57
+
58
+ first = np.cos(2 * gamma) ** (di - 1) + np.cos(2 * gamma) ** (dj - 1)
59
+ first *= 0.5 * np.sin(4 * beta) * np.sin(2 * gamma)
60
+
61
+ node_list = list(graph.nodes).copy()
62
+ node_list.remove(i)
63
+ node_list.remove(j)
64
+ f1 = 1
65
+ f2 = 1
66
+ for k in node_list:
67
+ if graph.has_edge(i, k) and graph.has_edge(j, k): # ijk is triangle
68
+ f1 *= np.cos(4 * gamma)
69
+ elif graph.has_edge(i, k) or graph.has_edge(j, k): # ijk is no triangle
70
+ f1 *= np.cos(2 * gamma)
71
+ f2 *= np.cos(2 * gamma)
72
+ second = 0.5 * np.sin(2 * beta) ** 2 * (f1 - f2)
73
+ return first - second
74
+
75
+ def get_expected_zz_edgedensity(x):
76
+ gamma = x[0]
77
+ beta = x[1]
78
+ # pylint: disable=consider-using-generator
79
+ return sum([get_Zij_maxcut_p1(edge, gamma, beta) for edge in graph.edges]) / graph.number_of_edges()
80
+
81
+ bounds = [(0.0, np.pi / 2), (-np.pi / 4, 0.0)]
82
+ x_init = [0.15, -0.28]
83
+
84
+ minimizer_kwargs = {"method": "L-BFGS-B", "bounds": bounds}
85
+ res = basinhopping(get_expected_zz_edgedensity, x_init, minimizer_kwargs=minimizer_kwargs, niter=10, T=2)
86
+
87
+ return res.x
88
+
89
+
90
+ def cut_cost_function(x: str, graph: Graph) -> int:
91
+ """Returns the number of cut edges in a graph (with minus sign).
92
+
93
+ Args:
94
+ x (str): solution bitstring.
95
+ graph (networkx graph): the MaxCut problem graph.
96
+
97
+ Returns:
98
+ obj (float): number of cut edges multiplied by -1.
99
+ """
100
+ obj = 0
101
+ for i, j in graph.edges():
102
+ if x[i] != x[j]:
103
+ obj += 1
104
+ return -1 * obj
105
+
106
+
107
+ def compute_expectation_value(
108
+ counts: Dict[str, int], graph: Graph, qubit_to_node: Dict[int, int], virtual_nodes: List[Tuple[int, int]]
109
+ ) -> float:
110
+ """Computes expectation value based on measurement results.
111
+
112
+ Args:
113
+ counts (Dict[str, int]): key as bitstring, val as count
114
+ graph (networkx) graph: the MaxCut problem graph
115
+ qubit_to_node (Dict[int, int]): mapping of qubit to nodes of the graph
116
+ virtual_nodes (List[Tuple[int, int]]): list of virtual nodes in the graph
117
+
118
+ Returns:
119
+ avg (float): expectation value of the cut edges for number of counts
120
+ """
121
+
122
+ avg = 0
123
+ sum_count = 0
124
+ for bitstring_aux, count in counts.items():
125
+ bitstring_aux_list = list(bitstring_aux)[::-1] # go from qiskit endianness to networkx endianness
126
+
127
+ # map the qubits back to nodes
128
+ bitstring = [""] * (len(bitstring_aux_list) + len(virtual_nodes))
129
+ for qubit, node in qubit_to_node.items():
130
+ bitstring[node] = bitstring_aux_list[qubit]
131
+
132
+ # insert virtual node(s) to bitstring
133
+ for virtual_node in virtual_nodes:
134
+ if virtual_node[0] is not None:
135
+ bitstring[virtual_node[0]] = str(virtual_node[1])
136
+
137
+ obj = cut_cost_function("".join(bitstring), graph)
138
+ avg += obj * count
139
+ sum_count += count
140
+
141
+ return avg / sum_count
142
+
143
+
144
+ def create_objective_function(
145
+ counts: Dict[str, int], graph: Graph, qubit_to_node: Dict[int, int], virtual_nodes: List[Tuple[int, int]]
146
+ ) -> Callable:
147
+ """
148
+ Creates a function that maps the parameters to the parametrized circuit,
149
+ runs it and computes the expectation value.
150
+
151
+ Args:
152
+ counts (Dict[str, int]): The dictionary of bitstring counts.
153
+ graph (networkx graph): the MaxCut problem graph.
154
+ qubit_to_node (Dict[int, int]): mapping of qubit to nodes of the graph
155
+ virtual_nodes (List[Tuple[int, int]]): list of virtual nodes in the graph
156
+ Returns:
157
+ callable: function that gives expectation value of the cut edges from counts sampled from the ansatz
158
+ """
159
+
160
+ def objective_function(temp):
161
+ temp = np.array(temp)
162
+ return compute_expectation_value(counts, graph, qubit_to_node, virtual_nodes)
163
+
164
+ return objective_function
165
+
166
+
167
+ def is_successful(
168
+ approximation_ratio: float,
169
+ ) -> bool:
170
+ """Check whether a Q-score benchmark returned approximation ratio above beta*, therefore being successful.
171
+
172
+ This condition checks that the mean approximation ratio is above the beta* = 0.2 threshold.
173
+
174
+ Args:
175
+ approximation_ratio (float): the mean approximation ratio of all problem graphs
176
+
177
+ Returns:
178
+ bool: whether the Q-score benchmark was successful
179
+ """
180
+ return bool(approximation_ratio > 0.2)
181
+
182
+
183
+ def plot_approximation_ratios(
184
+ nodes: list[int],
185
+ beta_ratio: list[float],
186
+ beta_std: list[float],
187
+ use_virtual_node: Optional[bool],
188
+ use_classically_optimized_angles: Optional[bool],
189
+ num_instances: int,
190
+ backend_name: str,
191
+ timestamp: str,
192
+ ) -> tuple[str, Figure]:
193
+ """Generate the figure of approximation ratios vs number of nodes,
194
+ including standard deviation and the acceptance threshold.
195
+
196
+ Args:
197
+ nodes (list[int]): list nodes for the problem graph sizes.
198
+ beta_ratio (list[float]): Beta ratio calculated for each graph size.
199
+ beta_std (list[float]): Standard deviation for beta ratio of each graph size.
200
+ use_virtual_node (Optional[bool]): whether to use virtual nodes or not.
201
+ use_classically_optimized_angles (Optional[bool]): whether to use classically optimized angles or not.
202
+ num_instances (int): the number of instances.
203
+ backend_name (str): the name of the backend.
204
+ timestamp (str): the timestamp of the execution of the experiment.
205
+
206
+ Returns:
207
+ str: the name of the figure.
208
+ Figure: the figure.
209
+ """
210
+
211
+ fig = plt.figure()
212
+ ax = plt.axes()
213
+
214
+ plt.axhline(0.2, color="red", linestyle="dashed", label="Threshold")
215
+ plt.errorbar(
216
+ nodes,
217
+ beta_ratio,
218
+ yerr=beta_std,
219
+ fmt="-o",
220
+ capsize=10,
221
+ markersize=8,
222
+ color="#759DEB",
223
+ label="Approximation ratio",
224
+ )
225
+
226
+ ax.set_ylabel(r"Q-score ratio $\beta(n)$")
227
+ ax.set_xlabel("Number of nodes $(n)$")
228
+ plt.xticks(range(min(nodes), max(nodes) + 1))
229
+ plt.legend(loc="lower right")
230
+ plt.grid(True)
231
+
232
+ if use_virtual_node and use_classically_optimized_angles:
233
+ title = f"Q-score, {num_instances} instances, with virtual node and classically optimized angles\nBackend: {backend_name} / {timestamp}"
234
+ elif use_virtual_node and not use_classically_optimized_angles:
235
+ title = f"Q-score, {num_instances} instances, with virtual node \nBackend: {backend_name} / {timestamp}"
236
+ elif not use_virtual_node and use_classically_optimized_angles:
237
+ title = f"Q-score, {num_instances} instances, with classically optimized angles\nBackend: {backend_name} / {timestamp}"
238
+ else:
239
+ title = f"Q-score, {num_instances} instances \nBackend: {backend_name} / {timestamp}"
240
+
241
+ plt.title(
242
+ title,
243
+ fontsize=9,
244
+ )
245
+ fig_name = f"{max(nodes)}_nodes_{num_instances}_instances.png"
246
+
247
+ # Show plot if verbose is True
248
+ plt.gcf().set_dpi(250)
249
+
250
+ plt.close()
251
+
252
+ return fig_name, fig
253
+
254
+
255
+ def qscore_analysis(run: BenchmarkRunResult) -> BenchmarkAnalysisResult:
256
+ """Analysis function for a QScore experiment
257
+
258
+ Args:
259
+ run (RunResult): A QScore experiment run for which analysis result is created
260
+ Returns:
261
+ AnalysisResult corresponding to QScore
262
+ """
263
+
264
+ plots = {}
265
+ observations: list[BenchmarkObservation] = []
266
+ dataset = run.dataset.copy(deep=True)
267
+
268
+ backend_name = dataset.attrs["backend_name"]
269
+ timestamp = dataset.attrs["execution_timestamp"]
270
+
271
+ max_num_nodes = dataset.attrs["max_num_nodes"]
272
+ min_num_nodes = dataset.attrs["min_num_nodes"]
273
+ num_instances: int = dataset.attrs["num_instances"]
274
+
275
+ use_virtual_node: bool = dataset.attrs["use_virtual_node"]
276
+ use_classically_optimized_angles = dataset.attrs["use_classically_optimized_angles"]
277
+ num_qaoa_layers = dataset.attrs["num_qaoa_layers"]
278
+
279
+ qscore = 0
280
+ nodes_list = list(range(min_num_nodes, max_num_nodes + 1))
281
+ beta_ratio_list = []
282
+ beta_ratio_std_list = []
283
+ for num_nodes in nodes_list:
284
+ # Retrieve counts for all the instances within each executed node size.
285
+ execution_results = xrvariable_to_counts(dataset, num_nodes, num_instances)
286
+
287
+ # Retrieve other dataset values
288
+ dataset_dictionary = dataset.attrs[num_nodes]
289
+
290
+ # node_set_list = dataset_dictionary["qubit_set"]
291
+ graph_list = dataset_dictionary["graph"]
292
+ qubit_to_node_list = dataset_dictionary["qubit_to_node"]
293
+ virtual_node_list = dataset_dictionary["virtual_nodes"]
294
+ no_edge_instances = dataset_dictionary["no_edge_instances"]
295
+
296
+ cut_sizes_list = [0.0] * len(no_edge_instances)
297
+ instances_with_edges = set(range(num_instances)) - set(no_edge_instances)
298
+
299
+ for inst_idx in list(instances_with_edges):
300
+ cut_sizes = run_QAOA(
301
+ execution_results[inst_idx],
302
+ graph_list[inst_idx],
303
+ qubit_to_node_list[inst_idx],
304
+ use_classically_optimized_angles,
305
+ num_qaoa_layers,
306
+ virtual_node_list[inst_idx],
307
+ )
308
+ cut_sizes_list.append(cut_sizes)
309
+
310
+ ## compute the approximation ratio beta
311
+ LAMBDA = 0.178
312
+
313
+ average_cut_size = np.mean(cut_sizes_list) - num_nodes * (num_nodes - 1) / 8
314
+ average_best_cut_size = 0.178 * pow(num_nodes, 3 / 2)
315
+ approximation_ratio = float(average_cut_size / average_best_cut_size)
316
+
317
+ approximation_ratio_list = [
318
+ (np.array(cut_sizes) - num_nodes * (num_nodes - 1) / 8) / (LAMBDA * num_nodes ** (3 / 2))
319
+ for cut_sizes in cut_sizes_list
320
+ ]
321
+ beta_ratio_list.append(np.mean(approximation_ratio_list))
322
+ success = is_successful(approximation_ratio)
323
+ std_of_approximation_ratio = np.std(np.array(approximation_ratio_list)) / np.sqrt(
324
+ len(approximation_ratio_list) - 1
325
+ )
326
+ beta_ratio_std_list.append(std_of_approximation_ratio)
327
+
328
+ if success:
329
+ qcvv_logger.info(
330
+ f"Q-Score = {num_nodes} passed with approximation ratio (Beta) {approximation_ratio:.4f}; Avg MaxCut size: {np.mean(cut_sizes_list):.4f}"
331
+ )
332
+ qscore = num_nodes
333
+ else:
334
+ qcvv_logger.info(
335
+ f"Q-Score = {num_nodes} failed with approximation ratio (Beta) {approximation_ratio:.4f} < 0.2; Avg MaxCut size: {np.mean(cut_sizes_list):.4f}"
336
+ )
337
+ observations.extend(
338
+ [
339
+ BenchmarkObservation(
340
+ name="approximation_ratio",
341
+ value=approximation_ratio,
342
+ uncertainty=std_of_approximation_ratio,
343
+ identifier=BenchmarkObservationIdentifier(num_nodes),
344
+ ),
345
+ BenchmarkObservation(
346
+ name="is_succesful",
347
+ value=str(success),
348
+ identifier=BenchmarkObservationIdentifier(num_nodes),
349
+ ),
350
+ BenchmarkObservation(
351
+ name="Qscore_result",
352
+ value=qscore if success else 1,
353
+ identifier=BenchmarkObservationIdentifier(num_nodes),
354
+ ),
355
+ ]
356
+ )
357
+
358
+ dataset.attrs[num_nodes].update(
359
+ {
360
+ "approximate_ratio_list": approximation_ratio_list,
361
+ }
362
+ )
363
+
364
+ fig_name, fig = plot_approximation_ratios(
365
+ nodes_list,
366
+ beta_ratio_list,
367
+ beta_ratio_std_list,
368
+ use_virtual_node,
369
+ use_classically_optimized_angles,
370
+ num_instances,
371
+ backend_name,
372
+ timestamp,
373
+ )
374
+ plots[fig_name] = fig
375
+
376
+ return BenchmarkAnalysisResult(dataset=dataset, plots=plots, observations=observations)
377
+
378
+
379
+ def run_QAOA(
380
+ counts: Dict[str, int],
381
+ graph_physical: Graph,
382
+ qubit_node: Dict[int, int],
383
+ use_classical_angles: bool,
384
+ qaoa_layers: int,
385
+ virtual_nodes: List[Tuple[int, int]],
386
+ ) -> float:
387
+ """
388
+ Solves the cut size of MaxCut for a graph using QAOA.
389
+ The result is average value sampled from the optimized ansatz.
390
+
391
+ Args:
392
+ counts (Dict[str, int]): key as bitstring, value as counts
393
+ graph_physical (Graph): the graph to be optimized
394
+ qubit_node (Dict[int, int]): the qubit to be optimized
395
+ use_classical_angles (bool): whether to use classical angles
396
+ qaoa_layers (int): the number of QAOA layers
397
+ virtual_nodes (List[Tuple[int, int]]): the presence of virtual nodes or not
398
+
399
+ Returns:
400
+ float: the expectation value of the maximum cut size.
401
+
402
+ """
403
+
404
+ objective_function = create_objective_function(counts, graph_physical, qubit_node, virtual_nodes)
405
+ if use_classical_angles:
406
+ if graph_physical.number_of_edges() != 0:
407
+ opt_angles = calculate_optimal_angles_for_QAOA_p1(graph_physical)
408
+ else:
409
+ opt_angles = [1.0, 1.0]
410
+ res = minimize(objective_function, opt_angles, method="COBYLA", tol=1e-5, options={"maxiter": 0})
411
+ else:
412
+ # Good initial angles from from Wurtz et.al. "The fixed angle conjecture for QAOA on regular MaxCut graphs." arXiv preprint arXiv:2107.00677 (2021).
413
+ OPTIMAL_INITIAL_ANGLES = {
414
+ "1": [-0.616, 0.393 / 2],
415
+ "2": [-0.488, 0.898 / 2, 0.555 / 2, 0.293 / 2],
416
+ "3": [-0.422, 0.798 / 2, 0.937 / 2, 0.609 / 2, 0.459 / 2, 0.235 / 2],
417
+ "4": [-0.409, 0.781 / 2, 0.988 / 2, 1.156 / 2, 0.600 / 2, 0.434 / 2, 0.297 / 2, 0.159 / 2],
418
+ "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],
419
+ "6": [
420
+ -0.331,
421
+ -0.645,
422
+ -0.731,
423
+ -0.837,
424
+ -1.009,
425
+ -1.126,
426
+ 0.636 / 2,
427
+ 0.535 / 2,
428
+ 0.463 / 2,
429
+ 0.360 / 2,
430
+ 0.259 / 2,
431
+ 0.139 / 2,
432
+ ],
433
+ "7": [
434
+ -0.310,
435
+ -0.618,
436
+ -0.690,
437
+ -0.751,
438
+ -0.859,
439
+ -1.020,
440
+ -1.122,
441
+ 0.648 / 2,
442
+ 0.554 / 2,
443
+ 0.490 / 2,
444
+ 0.445 / 2,
445
+ 0.341 / 2,
446
+ 0.244 / 2,
447
+ 0.131 / 2,
448
+ ],
449
+ "8": [
450
+ -0.295,
451
+ -0.587,
452
+ -0.654,
453
+ -0.708,
454
+ -0.765,
455
+ -0.864,
456
+ -1.026,
457
+ -1.116,
458
+ 0.649 / 2,
459
+ 0.555 / 2,
460
+ 0.500 / 2,
461
+ 0.469 / 2,
462
+ 0.420 / 2,
463
+ 0.319 / 2,
464
+ 0.231 / 2,
465
+ 0.123 / 2,
466
+ ],
467
+ "9": [
468
+ -0.279,
469
+ -0.566,
470
+ -0.631,
471
+ -0.679,
472
+ -0.726,
473
+ -0.768,
474
+ -0.875,
475
+ -1.037,
476
+ -1.118,
477
+ 0.654 / 2,
478
+ 0.562 / 2,
479
+ 0.509 / 2,
480
+ 0.487 / 2,
481
+ 0.451 / 2,
482
+ 0.403 / 2,
483
+ 0.305 / 2,
484
+ 0.220 / 2,
485
+ 0.117 / 2,
486
+ ],
487
+ "10": [
488
+ -0.267,
489
+ -0.545,
490
+ -0.610,
491
+ -0.656,
492
+ -0.696,
493
+ -0.729,
494
+ -0.774,
495
+ -0.882,
496
+ -1.044,
497
+ -1.115,
498
+ 0.656 / 2,
499
+ 0.563 / 2,
500
+ 0.514 / 2,
501
+ 0.496 / 2,
502
+ 0.496 / 2,
503
+ 0.436 / 2,
504
+ 0.388 / 2,
505
+ 0.291 / 2,
506
+ 0.211 / 2,
507
+ 0.112 / 2,
508
+ ],
509
+ "11": [
510
+ -0.257,
511
+ -0.528,
512
+ -0.592,
513
+ -0.640,
514
+ -0.677,
515
+ -0.702,
516
+ -0.737,
517
+ -0.775,
518
+ -0.884,
519
+ -1.047,
520
+ -1.115,
521
+ 0.656 / 2,
522
+ 0.563 / 2,
523
+ 0.516 / 2,
524
+ 0.504 / 2,
525
+ 0.482 / 2,
526
+ 0.456 / 2,
527
+ 0.421 / 2,
528
+ 0.371 / 2,
529
+ 0.276 / 2,
530
+ 0.201 / 2,
531
+ 0.107 / 2,
532
+ ],
533
+ }
534
+
535
+ theta = OPTIMAL_INITIAL_ANGLES[str(qaoa_layers)]
536
+ bounds = [(-np.pi, np.pi)] * qaoa_layers + [(0.0, np.pi)] * qaoa_layers
537
+
538
+ res = minimize(
539
+ objective_function,
540
+ theta,
541
+ bounds=bounds,
542
+ method="COBYLA",
543
+ tol=1e-5,
544
+ options={"maxiter": 300},
545
+ )
546
+
547
+ return -res.fun
548
+
549
+
550
+ class QScoreBenchmark(Benchmark):
39
551
  """
40
552
  Q-score estimates the size of combinatorial optimization problems a given number of qubits can execute with meaningful results.
41
553
  """
42
554
 
43
- def __init__(self, backend: IQMBackendBase, configuration: "QScoreConfiguration"):
555
+ analysis_function = staticmethod(qscore_analysis)
556
+
557
+ name: str = "qscore"
558
+
559
+ def __init__(self, backend_arg: IQMBackendBase, configuration: "QScoreConfiguration"):
44
560
  """Construct the QScoreBenchmark class.
45
561
 
46
562
  Args:
47
- backend (IQMBackendBase): the backend to execute the benchmark on
563
+ backend_arg (IQMBackendBase): the backend to execute the benchmark on
48
564
  configuration (QScoreConfiguration): the configuration of the benchmark
49
565
  """
50
- super().__init__(backend, configuration)
566
+ super().__init__(backend_arg, configuration)
567
+
568
+ self.backend_configuration_name = backend_arg if isinstance(backend_arg, str) else backend_arg.name
51
569
 
52
570
  self.num_instances = configuration.num_instances
53
571
  self.num_qaoa_layers = configuration.num_qaoa_layers
@@ -58,7 +576,8 @@ class QScoreBenchmark(BenchmarkBase):
58
576
  self.choose_qubits_routine = configuration.choose_qubits_routine
59
577
  self.qiskit_optim_level = configuration.qiskit_optim_level
60
578
  self.optimize_sqg = configuration.optimize_sqg
61
- self.timestamp = strftime("%Y%m%d-%H%M%S")
579
+ self.session_timestamp = strftime("%Y%m%d-%H%M%S")
580
+ self.execution_timestamp = ""
62
581
  self.seed = configuration.seed
63
582
 
64
583
  self.graph_physical: Graph
@@ -66,6 +585,11 @@ class QScoreBenchmark(BenchmarkBase):
66
585
  self.node_to_qubit: Dict[int, int]
67
586
  self.qubit_to_node: Dict[int, int]
68
587
 
588
+ # Initialize the variable to contain all QScore circuits
589
+ self.circuits = Circuits()
590
+ self.untranspiled_circuits = BenchmarkCircuit(name="untranspiled_circuits")
591
+ self.transpiled_circuits = BenchmarkCircuit(name="transpiled_circuits")
592
+
69
593
  if self.use_classically_optimized_angles and self.num_qaoa_layers > 1:
70
594
  raise ValueError("If the `use_classically_optimized_angles` is chosen, the `num_qaoa_layers` must be 1.")
71
595
 
@@ -73,11 +597,9 @@ class QScoreBenchmark(BenchmarkBase):
73
597
  raise ValueError("If the `use_virtual_node` is chosen, the `num_qaoa_layers` must be 1.")
74
598
 
75
599
  if self.choose_qubits_routine == "custom":
76
- self.custom_qubits_array = configuration.custom_qubits_array
77
-
78
- @staticmethod
79
- def name() -> str:
80
- return "qscore"
600
+ self.custom_qubits_array = [
601
+ list(x) for x in cast(Sequence[Sequence[int]], configuration.custom_qubits_array)
602
+ ]
81
603
 
82
604
  def generate_maxcut_ansatz( # pylint: disable=too-many-branches
83
605
  self,
@@ -96,8 +618,6 @@ class QScoreBenchmark(BenchmarkBase):
96
618
  gamma = theta[: self.num_qaoa_layers]
97
619
  beta = theta[self.num_qaoa_layers :]
98
620
 
99
- num_qubits = self.graph_physical.number_of_nodes()
100
-
101
621
  if self.graph_physical.number_of_nodes() != graph.number_of_nodes():
102
622
  num_qubits = self.graph_physical.number_of_nodes()
103
623
  # re-label the nodes to be between 0 and _num_qubits
@@ -111,7 +631,6 @@ class QScoreBenchmark(BenchmarkBase):
111
631
  # in case the graph is trivial: return empty circuit
112
632
  if num_qubits == 0:
113
633
  return QuantumCircuit(1)
114
-
115
634
  qaoa_qc = QuantumCircuit(num_qubits)
116
635
  for i in range(0, num_qubits):
117
636
  qaoa_qc.h(i)
@@ -136,335 +655,24 @@ class QScoreBenchmark(BenchmarkBase):
136
655
  for i in range(0, num_qubits):
137
656
  qaoa_qc.rx(2 * beta[layer], i)
138
657
  qaoa_qc.measure_all()
139
-
140
658
  return qaoa_qc
141
659
 
142
- @staticmethod
143
- def cost_function(x: str, graph: Graph) -> int:
144
- """Returns the number of cut edges in a graph (with minus sign).
145
-
146
- Args:
147
- x (str): solution bitstring.
148
- graph (networkx graph): the MaxCut problem graph.
149
-
150
- Returns:
151
- obj (float): number of cut edges multiplied by -1.
152
- """
153
-
154
- obj = 0
155
- for i, j in graph.edges():
156
- if x[i] != x[j]:
157
- obj += 1
158
-
159
- return -1 * obj
160
-
161
- def compute_expectation_value(self, counts: Dict[str, int], graph: Graph) -> float:
162
- """Computes expectation value based on measurement results.
163
-
164
- Args:
165
- counts (Dict[str, int]): key as bitstring, val as count
166
- graph (networkx) graph: the MaxCut problem graph
167
-
168
- Returns:
169
- avg (float): expectation value of the cut edges for number of counts
170
- """
171
-
172
- avg = 0
173
- sum_count = 0
174
- for bitstring_aux, count in counts.items():
175
- bitstring_aux_list = list(bitstring_aux)[::-1] # go from qiskit endianness to networkx endianness
176
-
177
- # map the qubits back to nodes
178
- bitstring = [""] * (len(bitstring_aux_list) + len(self.virtual_nodes))
179
- for qubit, node in self.qubit_to_node.items():
180
- bitstring[node] = bitstring_aux_list[qubit]
181
-
182
- # insert virtual node(s) to bitstring
183
- for virtual_node in self.virtual_nodes:
184
- if virtual_node[0] is not None:
185
- bitstring[virtual_node[0]] = str(virtual_node[1])
186
-
187
- obj = self.cost_function("".join(bitstring), graph)
188
- avg += obj * count
189
- sum_count += count
190
-
191
- return avg / sum_count
192
-
193
- def create_objective_function(self, graph: Graph, qubit_set: List[int]) -> Callable:
194
- """
195
- Creates a function that maps the parameters to the parametrized circuit,
196
- runs it and computes the expectation value.
197
-
198
- Args:
199
- graph (networkx graph): the MaxCut problem graph.
200
- qubit_set (List[int]): indices of the used qubits.
201
- Returns:
202
- callable: function that gives expectation value of the cut edges from counts sampled from the ansatz
203
- """
204
-
205
- def objective_function(theta):
206
- qc = self.generate_maxcut_ansatz(graph, theta)
207
-
208
- if len(qc.count_ops()) == 0:
209
- counts = {"": 1.0} # to handle the case of physical graph with no edges
210
-
211
- else:
212
- coupling_map = self.backend.coupling_map.reduce(qubit_set)
213
- qcvv_logger.setLevel(logging.WARNING)
214
- transpiled_qc_list, _ = perform_backend_transpilation(
215
- [qc],
216
- backend=self.backend,
217
- qubits=qubit_set,
218
- coupling_map=coupling_map,
219
- qiskit_optim_level=self.qiskit_optim_level,
220
- optimize_sqg=self.optimize_sqg,
221
- routing_method=self.routing_method,
222
- )
223
-
224
- sorted_transpiled_qc_list = {tuple(qubit_set): transpiled_qc_list}
225
- # Execute on the backend
226
- jobs, _ = submit_execute(
227
- sorted_transpiled_qc_list,
228
- self.backend,
229
- self.shots,
230
- self.calset_id,
231
- max_gates_per_batch=self.max_gates_per_batch,
232
- )
233
-
234
- counts = retrieve_all_counts(jobs)[0][0]
235
- qcvv_logger.setLevel(logging.INFO)
236
-
237
- return self.compute_expectation_value(counts, graph)
238
-
239
- return objective_function
240
-
241
- @staticmethod
242
- def calculate_optimal_angles_for_QAOA_p1(graph: Graph) -> List[float]:
243
- """
244
- Calculates the optimal angles for single layer QAOA MaxCut ansatz.
245
-
246
- Args:
247
- graph (networkx graph): the MaxCut problem graph.
248
-
249
- Returns:
250
- List[float]: optimal angles gamma and beta.
251
-
252
- """
253
-
254
- def get_Zij_maxcut_p1(edge_ij, gamma, beta):
255
- """
256
- Calculates <p1_QAOA | Z_i Z_j | p1_QAOA>, assuming ij is edge of G.
257
- """
258
- i, j = edge_ij
259
- di = graph.degree[i]
260
- dj = graph.degree[j]
261
-
262
- first = np.cos(2 * gamma) ** (di - 1) + np.cos(2 * gamma) ** (dj - 1)
263
- first *= 0.5 * np.sin(4 * beta) * np.sin(2 * gamma)
264
-
265
- node_list = list(graph.nodes).copy()
266
- node_list.remove(i)
267
- node_list.remove(j)
268
- f1 = 1
269
- f2 = 1
270
- for k in node_list:
271
- if graph.has_edge(i, k) and graph.has_edge(j, k): # ijk is triangle
272
- f1 *= np.cos(4 * gamma)
273
- elif graph.has_edge(i, k) or graph.has_edge(j, k): # ijk is no triangle
274
- f1 *= np.cos(2 * gamma)
275
- f2 *= np.cos(2 * gamma)
276
- second = 0.5 * np.sin(2 * beta) ** 2 * (f1 - f2)
277
- return first - second
278
-
279
- def get_expected_zz_edgedensity(x):
280
- gamma = x[0]
281
- beta = x[1]
282
- # pylint: disable=consider-using-generator
283
- return sum([get_Zij_maxcut_p1(edge, gamma, beta) for edge in graph.edges]) / graph.number_of_edges()
284
-
285
- bounds = [(0.0, np.pi / 2), (-np.pi / 4, 0.0)]
286
- x_init = [0.15, -0.28]
287
-
288
- minimizer_kwargs = {"method": "L-BFGS-B", "bounds": bounds}
289
- res = basinhopping(get_expected_zz_edgedensity, x_init, minimizer_kwargs=minimizer_kwargs, niter=10, T=2)
290
-
291
- return res.x
292
-
293
- def run_QAOA(self, graph: Graph, qubit_set: List[int]) -> float:
294
- """
295
- Solves the cut size of MaxCut for a graph using QAOA.
296
- The result is average value sampled from the optimized ansatz.
660
+ def add_all_meta_to_dataset(self, dataset: xr.Dataset):
661
+ """Adds all configuration metadata and circuits to the dataset variable
297
662
 
298
663
  Args:
299
- graph (networkx graph): the MaxCut problem graph.
300
- qubit_set (List[int]): indices of the used qubits.
301
-
302
- Returns:
303
- float: the expectation value of the maximum cut size.
304
-
664
+ dataset (xr.Dataset): The xarray dataset
305
665
  """
306
-
307
- objective_function = self.create_objective_function(graph, qubit_set)
308
-
309
- if self.use_classically_optimized_angles:
310
- if self.graph_physical.number_of_edges() != 0:
311
- opt_angles = self.calculate_optimal_angles_for_QAOA_p1(self.graph_physical)
666
+ dataset.attrs["session_timestamp"] = self.session_timestamp
667
+ dataset.attrs["execution_timestamp"] = self.execution_timestamp
668
+ dataset.attrs["backend_configuration_name"] = self.backend_configuration_name
669
+ dataset.attrs["backend_name"] = self.backend.name
670
+
671
+ for key, value in self.configuration:
672
+ if key == "benchmark": # Avoid saving the class object
673
+ dataset.attrs[key] = value.name
312
674
  else:
313
- opt_angles = [1.0, 1.0]
314
- res = minimize(objective_function, opt_angles, method="COBYLA", tol=1e-5, options={"maxiter": 0})
315
- else:
316
- # Good initial angles from from Wurtz et.al. "The fixed angle conjecture for QAOA on regular MaxCut graphs." arXiv preprint arXiv:2107.00677 (2021).
317
- OPTIMAL_INITIAL_ANGLES = {
318
- "1": [-0.616, 0.393 / 2],
319
- "2": [-0.488, 0.898 / 2, 0.555 / 2, 0.293 / 2],
320
- "3": [-0.422, 0.798 / 2, 0.937 / 2, 0.609 / 2, 0.459 / 2, 0.235 / 2],
321
- "4": [-0.409, 0.781 / 2, 0.988 / 2, 1.156 / 2, 0.600 / 2, 0.434 / 2, 0.297 / 2, 0.159 / 2],
322
- "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],
323
- "6": [
324
- -0.331,
325
- -0.645,
326
- -0.731,
327
- -0.837,
328
- -1.009,
329
- -1.126,
330
- 0.636 / 2,
331
- 0.535 / 2,
332
- 0.463 / 2,
333
- 0.360 / 2,
334
- 0.259 / 2,
335
- 0.139 / 2,
336
- ],
337
- "7": [
338
- -0.310,
339
- -0.618,
340
- -0.690,
341
- -0.751,
342
- -0.859,
343
- -1.020,
344
- -1.122,
345
- 0.648 / 2,
346
- 0.554 / 2,
347
- 0.490 / 2,
348
- 0.445 / 2,
349
- 0.341 / 2,
350
- 0.244 / 2,
351
- 0.131 / 2,
352
- ],
353
- "8": [
354
- -0.295,
355
- -0.587,
356
- -0.654,
357
- -0.708,
358
- -0.765,
359
- -0.864,
360
- -1.026,
361
- -1.116,
362
- 0.649 / 2,
363
- 0.555 / 2,
364
- 0.500 / 2,
365
- 0.469 / 2,
366
- 0.420 / 2,
367
- 0.319 / 2,
368
- 0.231 / 2,
369
- 0.123 / 2,
370
- ],
371
- "9": [
372
- -0.279,
373
- -0.566,
374
- -0.631,
375
- -0.679,
376
- -0.726,
377
- -0.768,
378
- -0.875,
379
- -1.037,
380
- -1.118,
381
- 0.654 / 2,
382
- 0.562 / 2,
383
- 0.509 / 2,
384
- 0.487 / 2,
385
- 0.451 / 2,
386
- 0.403 / 2,
387
- 0.305 / 2,
388
- 0.220 / 2,
389
- 0.117 / 2,
390
- ],
391
- "10": [
392
- -0.267,
393
- -0.545,
394
- -0.610,
395
- -0.656,
396
- -0.696,
397
- -0.729,
398
- -0.774,
399
- -0.882,
400
- -1.044,
401
- -1.115,
402
- 0.656 / 2,
403
- 0.563 / 2,
404
- 0.514 / 2,
405
- 0.496 / 2,
406
- 0.496 / 2,
407
- 0.436 / 2,
408
- 0.388 / 2,
409
- 0.291 / 2,
410
- 0.211 / 2,
411
- 0.112 / 2,
412
- ],
413
- "11": [
414
- -0.257,
415
- -0.528,
416
- -0.592,
417
- -0.640,
418
- -0.677,
419
- -0.702,
420
- -0.737,
421
- -0.775,
422
- -0.884,
423
- -1.047,
424
- -1.115,
425
- 0.656 / 2,
426
- 0.563 / 2,
427
- 0.516 / 2,
428
- 0.504 / 2,
429
- 0.482 / 2,
430
- 0.456 / 2,
431
- 0.421 / 2,
432
- 0.371 / 2,
433
- 0.276 / 2,
434
- 0.201 / 2,
435
- 0.107 / 2,
436
- ],
437
- }
438
-
439
- theta = OPTIMAL_INITIAL_ANGLES[str(self.num_qaoa_layers)]
440
- bounds = [(-np.pi, np.pi)] * self.num_qaoa_layers + [(0.0, np.pi)] * self.num_qaoa_layers
441
-
442
- res = minimize(
443
- objective_function,
444
- theta,
445
- bounds=bounds,
446
- method="COBYLA",
447
- tol=1e-5,
448
- options={"maxiter": 300},
449
- )
450
-
451
- return -res.fun
452
-
453
- @staticmethod
454
- def is_successful(
455
- approximation_ratio: float,
456
- ) -> bool:
457
- """Check whether a Q-score benchmark returned approximation ratio above beta*, therefore being successful.
458
-
459
- This condition checks that the mean approximation ratio is above the beta* = 0.2 threshold.
460
-
461
- Args:
462
- approximation_ratio (float): the mean approximation ratio of all problem graphs
463
-
464
- Returns:
465
- bool: whether the Q-score benchmark was successful
466
- """
467
- return bool(approximation_ratio > 0.2)
675
+ dataset.attrs[key] = value
468
676
 
469
677
  @staticmethod
470
678
  def choose_qubits_naive(num_qubits: int) -> list[int]:
@@ -502,171 +710,19 @@ class QScoreBenchmark(BenchmarkBase):
502
710
  # The execute_single_benchmark call must be looped through a COPY of custom_qubits_array
503
711
  else:
504
712
  chosen_qubits = selected_qubits[0]
505
- return chosen_qubits
506
-
507
- def plot_approximation_ratios(
508
- self, list_of_num_nodes: list[int], list_of_cut_sizes: list[list[float]]
509
- ) -> tuple[str, Figure]:
510
- """Generate the figure of approximation ratios vs number of nodes,
511
- including standard deviation and the acceptance threshold.
512
-
513
- Args:
514
- list_of_num_nodes (list[int]): list of problem graph sizes.
515
- list_of_cut_sizes (list[list[float]]): the list of lists of maximum average cut sizes of problem graph instances for each problem size.
516
-
517
- Returns:
518
- str: the name of the figure.
519
- Figure: the figure.
520
- """
521
-
522
- LAMBDA = 0.178
523
-
524
- approximation_ratio_lists = {
525
- num_nodes: (np.array(cut_sizes) - num_nodes * (num_nodes - 1) / 8) / (LAMBDA * num_nodes ** (3 / 2))
526
- for num_nodes, cut_sizes in zip(list_of_num_nodes, list_of_cut_sizes)
527
- }
713
+ return list(chosen_qubits)
528
714
 
529
- avg_approximation_ratios = {
530
- num_nodes: np.mean(approximation_ratios)
531
- for num_nodes, approximation_ratios in approximation_ratio_lists.items()
532
- }
533
-
534
- std_of_approximation_ratios = {
535
- num_nodes: np.std(approximation_ratios) / np.sqrt(len(approximation_ratios) - 1)
536
- for num_nodes, approximation_ratios in approximation_ratio_lists.items()
537
- }
538
-
539
- nodes = list(avg_approximation_ratios.keys())
540
- ratios = list(avg_approximation_ratios.values())
541
- std = list(std_of_approximation_ratios.values())
542
-
543
- fig = plt.figure()
544
- ax = plt.axes()
545
-
546
- plt.axhline(0.2, color="red", linestyle="dashed", label="Threshold")
547
- plt.errorbar(
548
- nodes, ratios, yerr=std, fmt="-o", capsize=10, markersize=8, color="#759DEB", label="Approximation ratio"
549
- )
550
-
551
- ax.set_ylabel(r"Q-score ratio $\beta(n)$")
552
- ax.set_xlabel("Number of nodes $(n)$")
553
- plt.xticks(range(min(nodes), max(nodes) + 1))
554
- plt.legend(loc="lower right")
555
- plt.grid(True)
556
-
557
- if self.use_virtual_node and self.use_classically_optimized_angles:
558
- title = f"Q-score, {self.num_instances} instances, with virtual node and classically optimized angles\nBackend: {self.backend.name} / {self.timestamp}"
559
- elif self.use_virtual_node and not self.use_classically_optimized_angles:
560
- title = f"Q-score, {self.num_instances} instances, with virtual node \nBackend: {self.backend.name} / {self.timestamp}"
561
- elif not self.use_virtual_node and self.use_classically_optimized_angles:
562
- title = f"Q-score, {self.num_instances} instances, with classically optimized angles\nBackend: {self.backend.name} / {self.timestamp}"
563
- else:
564
- title = f"Q-score, {self.num_instances} instances \nBackend: {self.backend.name} / {self.timestamp}"
565
-
566
- plt.title(
567
- title,
568
- fontsize=9,
569
- )
570
- fig_name = f"{max(nodes)}_nodes_{self.num_instances}_instances.png"
571
-
572
- # Show plot if verbose is True
573
- plt.gcf().set_dpi(250)
574
- plt.show()
575
-
576
- plt.close()
577
-
578
- return fig_name, fig
579
-
580
- def execute_single_benchmark(
715
+ def execute(
581
716
  self,
582
- num_nodes: int,
583
- ) -> tuple[bool, float, list[float], list[int]]:
584
- """Execute a single benchmark, for a given number of qubits.
585
-
586
- Args:
587
- num_nodes (int): number of nodes in the MaxCut problem graphs.
588
-
589
- Returns:
590
- bool: whether the benchmark was successful.
591
- float: approximation_ratio.
592
- list[float]: the list of maximum average cut sizes of problem graph instances.
593
- list[int]: the set of qubits the Q-score benchmark was executed on.
594
- """
595
-
596
- cut_sizes: list[float] = []
597
- seed = self.seed
717
+ backend: IQMBackendBase,
718
+ # pylint: disable=too-many-branches
719
+ # pylint: disable=too-many-statements
720
+ ) -> xr.Dataset:
721
+ """Executes the benchmark."""
722
+ self.execution_timestamp = strftime("%Y%m%d-%H%M%S")
598
723
 
599
- for i in range(self.num_instances):
600
- graph = nx.generators.erdos_renyi_graph(num_nodes, 0.5, seed=seed)
601
- qcvv_logger.debug(f"graph: {graph}")
602
- self.graph_physical = graph.copy()
603
- self.virtual_nodes = []
604
- if self.use_virtual_node:
605
- virtual_node, _ = max(
606
- graph.degree(), key=lambda x: x[1]
607
- ) # choose the virtual node as the most connected node
608
- self.virtual_nodes.append(
609
- (virtual_node, 1)
610
- ) # the second element of the tuple is the value assigned to the virtual node
611
- self.graph_physical.remove_node(self.virtual_nodes[0][0])
612
- # See if there are any non-connected nodes if so, remove them also
613
- # and set them to be opposite value to the possible original virtual node
614
- for node in self.graph_physical.nodes():
615
- if self.graph_physical.degree(node) == 0:
616
- self.virtual_nodes.append((node, 0))
617
- for vn in self.virtual_nodes:
618
- if self.graph_physical.has_node(vn[0]):
619
- self.graph_physical.remove_node(vn[0])
620
-
621
- # Graph with no edges has cut size = 0
622
- if graph.number_of_edges() == 0:
623
- cut_sizes.append(0)
624
- seed += 1
625
- qcvv_logger.debug(f"Graph {i+1}/{self.num_instances} had no edges: cut size = 0.")
626
- continue
627
-
628
- # Choose the qubit layout
629
- qubit_set = []
630
- if self.choose_qubits_routine.lower() == "naive":
631
- qubit_set = self.choose_qubits_naive(num_nodes)
632
- elif self.choose_qubits_routine.lower() == "custom":
633
- qubit_set = self.choose_qubits_custom(num_nodes)
634
- else:
635
- raise ValueError('choose_qubits_routine must either be "naive" or "custom".')
636
-
637
- # Solve the maximum cut size with QAOA
638
- cut_sizes.append(self.run_QAOA(graph, qubit_set))
639
- seed += 1
640
- qcvv_logger.debug(f"Solved the MaxCut on graph {i+1}/{self.num_instances}.")
641
-
642
- average_cut_size = np.mean(cut_sizes) - num_nodes * (num_nodes - 1) / 8
643
- average_best_cut_size = 0.178 * pow(num_nodes, 3 / 2)
644
- approximation_ratio = float(average_cut_size / average_best_cut_size)
645
-
646
- self.raw_data[num_nodes] = {
647
- "qubit_set": qubit_set,
648
- "cut_sizes": cut_sizes,
649
- }
650
- self.results[num_nodes] = {
651
- "qubit_set": qubit_set,
652
- "is_successful": str(self.is_successful(approximation_ratio)),
653
- "approximation_ratio": approximation_ratio,
654
- }
655
-
656
- # Return whether the single Q-score Benchmark was successful and its mean approximation ratio
657
- # and cut sizes for all instances.
658
- return self.is_successful(approximation_ratio), approximation_ratio, cut_sizes, qubit_set
659
-
660
- @timeit
661
- def execute_full_benchmark(self) -> tuple[int, list[float], list[list[float]]]:
662
- """Execute the full benchmark, starting with self.min_num_nodes nodes up to failure.
663
-
664
- Returns:
665
- int: the Q-score of the device.
666
- list[float]: the list of approximation rations over problem graph instances for each problem size.
667
- list[list[float]]: the list of lists of maximum average cut sizes of problem graph instances for each problem size.
668
- """
669
- qscore = 0
724
+ dataset = xr.Dataset()
725
+ self.add_all_meta_to_dataset(dataset)
670
726
 
671
727
  if self.max_num_nodes is None:
672
728
  if self.use_virtual_node:
@@ -676,38 +732,158 @@ class QScoreBenchmark(BenchmarkBase):
676
732
  else:
677
733
  max_num_nodes = self.max_num_nodes
678
734
 
679
- approximation_ratios = []
680
- list_of_cut_sizes = []
735
+ dataset.attrs.update({"max_num_nodes": self.max_num_nodes})
681
736
 
682
737
  for num_nodes in range(self.min_num_nodes, max_num_nodes + 1):
738
+ qc_list = []
739
+ qc_transpiled_list: List[QuantumCircuit] = []
740
+ execution_results = []
741
+ graph_list = []
742
+ qubit_set_list = []
743
+
683
744
  qcvv_logger.debug(f"Executing on {self.num_instances} random graphs with {num_nodes} nodes.")
684
- is_successful, approximation_ratio, cut_sizes = self.execute_single_benchmark(num_nodes)[0:3]
685
- approximation_ratios.append(approximation_ratio)
686
745
 
687
- list_of_cut_sizes.append(cut_sizes)
688
- if is_successful:
689
- qcvv_logger.info(
690
- f"Q-Score = {num_nodes} passed with:\nApproximation ratio (Beta): {approximation_ratio:.4f}; Avg MaxCut size: {np.mean(cut_sizes):.4f}"
691
- )
692
- qscore = num_nodes
693
- continue
746
+ # self.untranspiled_circuits[str(num_nodes)] = {}
747
+ # self.transpiled_circuits[str(num_nodes)] = {}
748
+
749
+ seed = self.seed
750
+ virtual_node_list = []
751
+ qubit_to_node_list = []
752
+ no_edge_instances = []
753
+ for instance in range(self.num_instances):
754
+ qcvv_logger.debug(f"Executing graph {instance} with {num_nodes} nodes.")
755
+ graph = nx.generators.erdos_renyi_graph(num_nodes, 0.5, seed=seed)
756
+ graph_list.append(graph)
757
+ self.graph_physical = graph.copy()
758
+ self.virtual_nodes = []
759
+ if self.use_virtual_node:
760
+ virtual_node, _ = max(
761
+ graph.degree(), key=lambda x: x[1]
762
+ ) # choose the virtual node as the most connected node
763
+ self.virtual_nodes.append(
764
+ (virtual_node, 1)
765
+ ) # the second element of the tuple is the value assigned to the virtual node
766
+ self.graph_physical.remove_node(self.virtual_nodes[0][0])
767
+ # See if there are any non-connected nodes if so, remove them also
768
+ # and set them to be opposite value to the possible original virtual node
769
+ for node in self.graph_physical.nodes():
770
+ if self.graph_physical.degree(node) == 0:
771
+ self.virtual_nodes.append((node, 0))
772
+ for vn in self.virtual_nodes:
773
+ if self.graph_physical.has_node(vn[0]):
774
+ self.graph_physical.remove_node(vn[0])
775
+ virtual_node_list.append(self.virtual_nodes)
776
+ # Graph with no edges has cut size = 0
777
+ if graph.number_of_edges() == 0:
778
+ no_edge_instances.append(instance)
779
+ qcvv_logger.debug(f"Graph {instance+1}/{self.num_instances} had no edges: cut size = 0.")
780
+
781
+ # Choose the qubit layout
782
+
783
+ if self.choose_qubits_routine.lower() == "naive":
784
+ qubit_set = self.choose_qubits_naive(num_nodes)
785
+ elif (
786
+ self.choose_qubits_routine.lower() == "custom" or self.choose_qubits_routine.lower() == "mapomatic"
787
+ ):
788
+ qubit_set = self.choose_qubits_custom(num_nodes)
789
+ else:
790
+ raise ValueError('choose_qubits_routine must either be "naive" or "custom".')
791
+ qubit_set_list.append(qubit_set)
792
+
793
+ qc = self.generate_maxcut_ansatz(graph, theta=[float(q) for q in qubit_set])
794
+ qc_list.append(qc)
795
+ qubit_to_node_copy = self.qubit_to_node.copy()
796
+ qubit_to_node_list.append(qubit_to_node_copy)
797
+
798
+ if len(qc.count_ops()) == 0:
799
+ counts = {"": 1.0} # to handle the case of physical graph with no edges
800
+ qc_transpiled_list.append([])
801
+ execution_results.append(counts)
802
+ qc_list.append([])
803
+ qcvv_logger.debug(f"This graph instance has no edges.")
804
+ else:
805
+ # execute for a given num_node and a given instance
806
+ coupling_map = self.backend.coupling_map.reduce(qubit_set)
807
+ qcvv_logger.setLevel(logging.WARNING)
808
+ transpiled_qc, _ = perform_backend_transpilation(
809
+ [qc],
810
+ backend=self.backend,
811
+ qubits=qubit_set,
812
+ coupling_map=coupling_map,
813
+ qiskit_optim_level=self.qiskit_optim_level,
814
+ optimize_sqg=self.optimize_sqg,
815
+ routing_method=self.routing_method,
816
+ )
817
+
818
+ sorted_transpiled_qc_list = {tuple(qubit_set): transpiled_qc}
819
+ # Execute on the backend
820
+ jobs, _ = submit_execute(
821
+ sorted_transpiled_qc_list,
822
+ self.backend,
823
+ self.shots,
824
+ self.calset_id,
825
+ max_gates_per_batch=self.max_gates_per_batch,
826
+ )
827
+ qc_transpiled_list.append(transpiled_qc)
828
+ execution_results.append(retrieve_all_counts(jobs)[0][0])
829
+ qcvv_logger.setLevel(logging.INFO)
694
830
 
695
- qcvv_logger.info(
696
- f"Q-Score = {num_nodes} failed with \napproximation ratio (Beta): {approximation_ratio:.4f} < 0.2; Avg MaxCut size: {np.mean(cut_sizes):.4f}"
831
+ seed += 1
832
+ qcvv_logger.debug(f"Solved the MaxCut on graph {instance+1}/{self.num_instances}.")
833
+
834
+ dataset.attrs.update(
835
+ {
836
+ num_nodes: {
837
+ "qubit_set": qubit_set_list,
838
+ "seed_start": seed,
839
+ "graph": graph_list,
840
+ "virtual_nodes": virtual_node_list,
841
+ "qubit_to_node": qubit_to_node_list,
842
+ "no_edge_instances": no_edge_instances,
843
+ }
844
+ }
697
845
  )
698
846
 
699
- self.results["qscore"] = qscore
700
- fig_name, fig = self.plot_approximation_ratios(
701
- list(range(self.min_num_nodes, max_num_nodes + 1)), list_of_cut_sizes
702
- )
703
- self.figures[fig_name] = fig
704
- return num_nodes, approximation_ratios, list_of_cut_sizes
847
+ qcvv_logger.debug(f"Adding counts for the random graph for {num_nodes} nodes to the dataset")
848
+ dataset, _ = add_counts_to_dataset(execution_results, str(num_nodes), dataset)
849
+
850
+ # self.untranspiled_circuits[str(num_nodes)].update({tuple(qubit_set): qc_list})
851
+ # self.transpiled_circuits[str(num_nodes)].update(sorted_transpiled_qc_list)
852
+ self.untranspiled_circuits.circuit_groups.append(CircuitGroup(name=str(num_nodes), circuits=qc_list))
853
+ self.transpiled_circuits.circuit_groups.append(
854
+ CircuitGroup(name=str(num_nodes), circuits=qc_transpiled_list)
855
+ )
856
+
857
+ self.circuits = Circuits([self.transpiled_circuits, self.untranspiled_circuits])
858
+
859
+ return dataset
705
860
 
706
861
 
707
862
  class QScoreConfiguration(BenchmarkConfigurationBase):
708
- """Q-score configuration."""
863
+ """Q-score configuration.
864
+
865
+ Attributes:
866
+ benchmark (Type[Benchmark]): QScoreBenchmark
867
+ num_instances (int):
868
+ num_qaoa_layers (int):
869
+ min_num_nodes (int):
870
+ max_num_nodes (int):
871
+ use_virtual_node (bool):
872
+ use_classically_optimized_angles (bool):
873
+ choose_qubits_routine (Literal["custom"]): The routine to select qubit layouts.
874
+ * Default is "custom".
875
+ min_num_qubits (int):
876
+ custom_qubits_array (Optional[Sequence[Sequence[int]]]): The physical qubit layouts to perform the benchmark on.
877
+ * Default is None.
878
+ qiskit_optim_level (int): The Qiskit transpilation optimization level.
879
+ * Default is 3.
880
+ optimize_sqg (bool): Whether Single Qubit Gate Optimization is performed upon transpilation.
881
+ * Default is True.
882
+ seed (int): The random seed.
883
+ * Default is 1.
884
+ """
709
885
 
710
- benchmark: Type[BenchmarkBase] = QScoreBenchmark
886
+ benchmark: Type[Benchmark] = QScoreBenchmark
711
887
  num_instances: int
712
888
  num_qaoa_layers: int = 1
713
889
  min_num_nodes: int = 2
@@ -716,7 +892,7 @@ class QScoreConfiguration(BenchmarkConfigurationBase):
716
892
  use_classically_optimized_angles: bool = True
717
893
  choose_qubits_routine: Literal["naive", "custom"] = "naive"
718
894
  min_num_qubits: int = 2 # If choose_qubits_routine is "naive"
719
- custom_qubits_array: Optional[list[list[int]]] = None
895
+ custom_qubits_array: Optional[Sequence[Sequence[int]]] = None
720
896
  qiskit_optim_level: int = 3
721
897
  optimize_sqg: bool = True
722
898
  seed: int = 1