iqm-benchmarks 2.26__py3-none-any.whl → 2.28__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 +2 -0
- iqm/benchmarks/compressive_gst/gst_analysis.py +6 -2
- iqm/benchmarks/entanglement/__init__.py +2 -1
- iqm/benchmarks/entanglement/ghz.py +10 -10
- iqm/benchmarks/entanglement/graph_states.py +1348 -0
- iqm/benchmarks/optimization/qscore.py +1 -1
- iqm/benchmarks/quantum_volume/clops.py +1 -1
- iqm/benchmarks/quantum_volume/quantum_volume.py +3 -6
- iqm/benchmarks/randomized_benchmarking/clifford_1q.pkl +0 -0
- iqm/benchmarks/randomized_benchmarking/clifford_2q.pkl +0 -0
- iqm/benchmarks/randomized_benchmarking/mirror_rb/mirror_rb.py +1 -1
- iqm/benchmarks/randomized_benchmarking/randomized_benchmarking_common.py +33 -8
- iqm/benchmarks/utils.py +313 -236
- iqm/benchmarks/utils_plots.py +233 -0
- iqm/benchmarks/utils_shadows.py +228 -0
- {iqm_benchmarks-2.26.dist-info → iqm_benchmarks-2.28.dist-info}/METADATA +6 -4
- {iqm_benchmarks-2.26.dist-info → iqm_benchmarks-2.28.dist-info}/RECORD +21 -18
- {iqm_benchmarks-2.26.dist-info → iqm_benchmarks-2.28.dist-info}/WHEEL +1 -1
- mGST/algorithm.py +1 -1
- {iqm_benchmarks-2.26.dist-info → iqm_benchmarks-2.28.dist-info}/licenses/LICENSE +0 -0
- {iqm_benchmarks-2.26.dist-info → iqm_benchmarks-2.28.dist-info}/top_level.txt +0 -0
iqm/benchmarks/utils.py
CHANGED
|
@@ -15,37 +15,35 @@
|
|
|
15
15
|
"""
|
|
16
16
|
General utility functions
|
|
17
17
|
"""
|
|
18
|
-
|
|
19
18
|
from collections import defaultdict
|
|
20
|
-
from dataclasses import dataclass
|
|
21
19
|
from functools import wraps
|
|
20
|
+
import itertools
|
|
22
21
|
from math import floor
|
|
23
22
|
import os
|
|
24
23
|
from time import time
|
|
25
|
-
from typing import Any, Dict, Iterable, List, Literal, Optional, Sequence, Tuple, Union, cast
|
|
24
|
+
from typing import Any, Dict, Iterable, List, Literal, Optional, Sequence, Set, Tuple, Union, cast
|
|
25
|
+
import warnings
|
|
26
26
|
|
|
27
|
-
import matplotlib.pyplot as plt
|
|
28
27
|
from more_itertools import chunked
|
|
29
28
|
from mthree.utils import final_measurement_mapping
|
|
30
29
|
import numpy as np
|
|
31
30
|
from numpy.random import Generator
|
|
32
31
|
from qiskit import ClassicalRegister, transpile
|
|
33
32
|
from qiskit.converters import circuit_to_dag
|
|
33
|
+
from qiskit.quantum_info import Pauli
|
|
34
34
|
from qiskit.transpiler import CouplingMap
|
|
35
35
|
import requests
|
|
36
|
-
from rustworkx import PyGraph, spring_layout, visualization # pylint: disable=no-name-in-module
|
|
37
36
|
import xarray as xr
|
|
38
37
|
|
|
39
38
|
from iqm.benchmarks.logging_config import qcvv_logger
|
|
40
39
|
from iqm.iqm_client.models import CircuitCompilationOptions
|
|
41
40
|
from iqm.qiskit_iqm import IQMCircuit as QuantumCircuit
|
|
42
|
-
from iqm.qiskit_iqm import transpile_to_IQM
|
|
41
|
+
from iqm.qiskit_iqm import IQMFakeDeneb, transpile_to_IQM
|
|
43
42
|
from iqm.qiskit_iqm.fake_backends.fake_adonis import IQMFakeAdonis
|
|
44
43
|
from iqm.qiskit_iqm.fake_backends.fake_apollo import IQMFakeApollo
|
|
45
44
|
from iqm.qiskit_iqm.iqm_backend import IQMBackendBase
|
|
46
45
|
from iqm.qiskit_iqm.iqm_job import IQMJob
|
|
47
46
|
from iqm.qiskit_iqm.iqm_provider import IQMProvider
|
|
48
|
-
from iqm.qiskit_iqm.iqm_transpilation import optimize_single_qubit_gates
|
|
49
47
|
|
|
50
48
|
|
|
51
49
|
def timeit(f):
|
|
@@ -169,6 +167,9 @@ def count_native_gates(
|
|
|
169
167
|
backend = backend_arg
|
|
170
168
|
|
|
171
169
|
native_operations = backend.operation_names
|
|
170
|
+
|
|
171
|
+
if "move" in backend.architecture.gates:
|
|
172
|
+
native_operations.append("move")
|
|
172
173
|
# Some backends may not include "barrier" in the operation_names attribute
|
|
173
174
|
if "barrier" not in native_operations:
|
|
174
175
|
native_operations.append("barrier")
|
|
@@ -194,6 +195,92 @@ def count_native_gates(
|
|
|
194
195
|
return avg_native_operations
|
|
195
196
|
|
|
196
197
|
|
|
198
|
+
@timeit
|
|
199
|
+
def generate_state_tomography_circuits(
|
|
200
|
+
qc: QuantumCircuit,
|
|
201
|
+
active_qubits: Sequence[int],
|
|
202
|
+
measure_other: Optional[Sequence[int]] = None,
|
|
203
|
+
measure_other_name: Optional[str] = None,
|
|
204
|
+
native: bool = True,
|
|
205
|
+
) -> Dict[str, QuantumCircuit]:
|
|
206
|
+
"""Generate all quantum circuits required for a quantum state tomography experiment.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
qc (QuantumCircuit): The quantum circuit.
|
|
210
|
+
active_qubits (Sequence[int]): The qubits to perform tomograhy on.
|
|
211
|
+
measure_other (Optional[Sequence[int]]): Whether to measure other qubits in the qc QuantumCircuit.
|
|
212
|
+
* Default is None.
|
|
213
|
+
measure_other_name (Optional[str]): Name of the classical register to assign measure_other.
|
|
214
|
+
native (bool): Whether circuits are prepared using IQM-native gates.
|
|
215
|
+
* Default is True.
|
|
216
|
+
Returns:
|
|
217
|
+
Dict[str, QuantumCircuit]: A dictionary with keys being Pauli (measurement) strings and values the respective circuit.
|
|
218
|
+
* Pauli strings are ordered for qubit labels in increasing order, e.g., "XY" for active_qubits 4, 1 corresponds to "X" measurement on qubit 1 and "Y" measurement on qubit 4.
|
|
219
|
+
"""
|
|
220
|
+
num_qubits = len(active_qubits)
|
|
221
|
+
|
|
222
|
+
# Organize all Pauli measurements as circuits
|
|
223
|
+
aux_circ = QuantumCircuit(1)
|
|
224
|
+
sqg_pauli_strings = ("Z", "X", "Y")
|
|
225
|
+
pauli_measurements = {p: aux_circ.copy() for p in sqg_pauli_strings}
|
|
226
|
+
|
|
227
|
+
# Avoid transpilation, generate either directly in native basis or in H, S
|
|
228
|
+
if native:
|
|
229
|
+
# Z measurement
|
|
230
|
+
pauli_measurements["Z"].r(0, 0, 0)
|
|
231
|
+
# X measurement
|
|
232
|
+
pauli_measurements["X"].r(np.pi / 2, np.pi / 2, 0)
|
|
233
|
+
pauli_measurements["X"].r(np.pi, 0, 0)
|
|
234
|
+
# Y measurement
|
|
235
|
+
pauli_measurements["Y"].r(-np.pi / 2, 0, 0)
|
|
236
|
+
pauli_measurements["Y"].r(np.pi, np.pi / 4, 0)
|
|
237
|
+
else:
|
|
238
|
+
# Z measurement
|
|
239
|
+
pauli_measurements["Z"].id(0)
|
|
240
|
+
# X measurement
|
|
241
|
+
pauli_measurements["X"].h(0)
|
|
242
|
+
# Y measurement
|
|
243
|
+
pauli_measurements["Y"].sdg(0)
|
|
244
|
+
pauli_measurements["Y"].h(0)
|
|
245
|
+
|
|
246
|
+
all_pauli_labels = ["".join(x) for x in itertools.product(sqg_pauli_strings, repeat=num_qubits)]
|
|
247
|
+
all_circuits = {P_n: qc.copy() for P_n in all_pauli_labels}
|
|
248
|
+
for P_n in all_pauli_labels:
|
|
249
|
+
all_circuits[P_n].barrier()
|
|
250
|
+
for q_idx, q_active in enumerate(sorted(active_qubits)):
|
|
251
|
+
all_circuits[P_n].compose(pauli_measurements[P_n[q_idx]], qubits=q_active, inplace=True)
|
|
252
|
+
|
|
253
|
+
all_circuits[P_n].barrier()
|
|
254
|
+
|
|
255
|
+
register_tomo = ClassicalRegister(len(active_qubits), "tomo_qubits")
|
|
256
|
+
all_circuits[P_n].add_register(register_tomo)
|
|
257
|
+
all_circuits[P_n].measure(active_qubits, register_tomo)
|
|
258
|
+
|
|
259
|
+
if measure_other is not None:
|
|
260
|
+
if measure_other_name is None:
|
|
261
|
+
measure_other_name = "non_tomo_qubits"
|
|
262
|
+
register_neighbors = ClassicalRegister(len(measure_other), measure_other_name)
|
|
263
|
+
all_circuits[P_n].add_register(register_neighbors)
|
|
264
|
+
all_circuits[P_n].measure(measure_other, register_neighbors)
|
|
265
|
+
|
|
266
|
+
return all_circuits
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def get_active_qubits(qc: QuantumCircuit) -> List[int]:
|
|
270
|
+
"""Extract active qubits from a quantum circuit.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
qc (QuantumCircuit): The quantum circuit to extract active qubits from.
|
|
274
|
+
Returns:
|
|
275
|
+
List[int]: A list of active qubits.
|
|
276
|
+
"""
|
|
277
|
+
active_qubits = set()
|
|
278
|
+
for instruction in qc.data:
|
|
279
|
+
for qubit in instruction.qubits:
|
|
280
|
+
active_qubits.add(qc.find_bit(qubit).index)
|
|
281
|
+
return list(active_qubits)
|
|
282
|
+
|
|
283
|
+
|
|
197
284
|
# pylint: disable=too-many-branches
|
|
198
285
|
def get_iqm_backend(backend_label: str) -> IQMBackendBase:
|
|
199
286
|
"""Get the IQM backend object from a backend name (str).
|
|
@@ -229,6 +316,9 @@ def get_iqm_backend(backend_label: str) -> IQMBackendBase:
|
|
|
229
316
|
iqm_server_url = "https://cocos.resonance.meetiqm.com/deneb"
|
|
230
317
|
provider = IQMProvider(iqm_server_url)
|
|
231
318
|
backend_object = provider.get_backend()
|
|
319
|
+
# FakeDeneb
|
|
320
|
+
elif backend_label.lower() in ("iqmfakedeneb", "fakedeneb"):
|
|
321
|
+
backend_object = IQMFakeDeneb()
|
|
232
322
|
|
|
233
323
|
else:
|
|
234
324
|
raise ValueError(f"Backend {backend_label} not supported. Try 'garnet', 'deneb', 'fakeadonis' or 'fakeapollo'.")
|
|
@@ -236,34 +326,160 @@ def get_iqm_backend(backend_label: str) -> IQMBackendBase:
|
|
|
236
326
|
return backend_object
|
|
237
327
|
|
|
238
328
|
|
|
239
|
-
def
|
|
240
|
-
"""
|
|
329
|
+
def get_measurement_mapping(circuit: QuantumCircuit) -> Dict[int, int]:
|
|
330
|
+
"""
|
|
331
|
+
Extracts the final measurement mapping (qubits to bits) of a quantum circuit.
|
|
332
|
+
|
|
333
|
+
Parameters:
|
|
334
|
+
circuit (QuantumCircuit): The quantum circuit to extract the measurement mapping from.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
dict: A dictionary where keys are qubits and values are classical bits.
|
|
338
|
+
"""
|
|
339
|
+
mapping = {}
|
|
340
|
+
for instruction, qargs, cargs in circuit.data:
|
|
341
|
+
if instruction.name == "measure":
|
|
342
|
+
qubit = circuit.find_bit(qargs[0]).registers[0][1]
|
|
343
|
+
cbit = circuit.find_bit(cargs[0]).registers[0][1]
|
|
344
|
+
mapping[qubit] = cbit
|
|
345
|
+
return mapping
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def get_neighbors_of_edges(edges: Sequence[Sequence[int]], graph: Sequence[Sequence[int]]) -> Set[int]:
|
|
349
|
+
"""Given a Sequence of edges and a graph, return all neighboring nodes of the edges.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
edges (Sequence[Sequence[int]]): A sequence of pairs of integers, representing edges of a graph.
|
|
353
|
+
graph (Sequence[Sequence[int]]): The input graph specified as a sequence of edges (Sequence[int]).
|
|
354
|
+
Returns:
|
|
355
|
+
Sequence[int]: list of all neighboring nodes of the input edges.
|
|
356
|
+
"""
|
|
357
|
+
neighboring_nodes = set()
|
|
358
|
+
nodes_in_edges = set()
|
|
359
|
+
|
|
360
|
+
for u, v in edges:
|
|
361
|
+
nodes_in_edges.add(u)
|
|
362
|
+
nodes_in_edges.add(v)
|
|
363
|
+
|
|
364
|
+
for x, y in graph:
|
|
365
|
+
if x in nodes_in_edges:
|
|
366
|
+
neighboring_nodes.add(y)
|
|
367
|
+
if y in nodes_in_edges:
|
|
368
|
+
neighboring_nodes.add(x)
|
|
369
|
+
neighboring_nodes -= nodes_in_edges
|
|
370
|
+
|
|
371
|
+
return neighboring_nodes
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def get_Pauli_expectation(counts: Dict[str, int], pauli_label: Literal["I", "X", "Y", "Z"]) -> float:
|
|
375
|
+
"""Gets an estimate of a Pauli expectation value for a given set of counts and a Pauli measurement label.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
counts (Dict[str, int]): A dictionary of counts.
|
|
379
|
+
* NB: keys are assumed to have a single bitstring, i.e., coming from a single classical register.
|
|
380
|
+
pauli_label (str): A Pauli measurement label, specified as a string of I, X, Y, Z characters.
|
|
381
|
+
|
|
382
|
+
Raises:
|
|
383
|
+
ValueError: If Pauli labels are not specified in terms of I, X, Y, Z characters.
|
|
384
|
+
Returns:
|
|
385
|
+
float: The estimate of the Pauli expectation value.
|
|
386
|
+
"""
|
|
387
|
+
num_qubits = len(list(counts.keys())[0])
|
|
388
|
+
sqg_pauli_strings = ("I", "Z", "X", "Y")
|
|
389
|
+
all_pauli_labels = ["".join(x) for x in itertools.product(sqg_pauli_strings, repeat=num_qubits)]
|
|
390
|
+
|
|
391
|
+
if pauli_label not in all_pauli_labels:
|
|
392
|
+
raise ValueError("pauli_label must be specified as a string made up of characters 'I', 'X', 'Y', or 'Z'.")
|
|
393
|
+
|
|
394
|
+
expect = 0
|
|
395
|
+
if "I" not in pauli_label:
|
|
396
|
+
for b, count_b in counts.items():
|
|
397
|
+
if b.count("1") % 2 == 0:
|
|
398
|
+
expect += count_b
|
|
399
|
+
else:
|
|
400
|
+
expect -= count_b
|
|
401
|
+
return expect / sum(counts.values())
|
|
402
|
+
|
|
403
|
+
non_I_indices = [idx for idx, P in enumerate(pauli_label) if P != "I"]
|
|
404
|
+
for b, count_b in counts.items():
|
|
405
|
+
b_Z_parity = [1 if b[i] == "1" else 0 for i in non_I_indices]
|
|
406
|
+
if sum(b_Z_parity) % 2 == 0:
|
|
407
|
+
expect += count_b
|
|
408
|
+
else:
|
|
409
|
+
expect -= count_b
|
|
410
|
+
return expect / sum(counts.values())
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def get_tomography_matrix(pauli_expectations: Dict[str, float]) -> np.ndarray:
|
|
414
|
+
"""Reconstructs a density matrix from given Pauli expectations.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
pauli_expectations (Dict[str, float]): A dictionary of Pauli expectations, with keys being Pauli strings.
|
|
418
|
+
Raises:
|
|
419
|
+
ValueError: If not all 4**n Pauli expectations are specified.
|
|
420
|
+
Returns:
|
|
421
|
+
np.ndarray: A tomographically reconstructed density matrix.
|
|
422
|
+
"""
|
|
423
|
+
num_qubits = len(list(pauli_expectations.keys())[0])
|
|
424
|
+
sqg_pauli_strings = ("I", "Z", "X", "Y")
|
|
425
|
+
all_pauli_labels = ["".join(x) for x in itertools.product(sqg_pauli_strings, repeat=num_qubits)]
|
|
426
|
+
if set(list(pauli_expectations.keys())) != set(all_pauli_labels):
|
|
427
|
+
raise ValueError(
|
|
428
|
+
f"Pauli expectations are incomplete ({len(list(pauli_expectations.keys()))} out of {len(all_pauli_labels)} expectations)"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
rho = np.zeros([2**num_qubits, 2**num_qubits], dtype=complex)
|
|
432
|
+
for pauli_string, pauli_expectation in pauli_expectations.items():
|
|
433
|
+
rho += 2 ** (-num_qubits) * pauli_expectation * Pauli(pauli_string).to_matrix()
|
|
434
|
+
return rho
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def marginal_distribution(prob_dist_or_counts: Dict[str, float | int], indices: Iterable[int]) -> Dict[str, float]:
|
|
438
|
+
"""Compute the marginal distribution over specified bits (indices).
|
|
241
439
|
|
|
242
440
|
Params:
|
|
243
|
-
- prob_dist (
|
|
244
|
-
- indices (
|
|
441
|
+
- prob_dist (Dict[str, float | int]): A dictionary with keys being bitstrings and values are either probabilities or counts
|
|
442
|
+
- indices (Iterable[int]): List of bit indices to marginalize over
|
|
245
443
|
|
|
246
444
|
Returns:
|
|
247
445
|
- dict: A dictionary representing the marginal distribution over the specified bits.
|
|
248
446
|
"""
|
|
249
447
|
marginal_dist: Dict[str, float] = defaultdict(float)
|
|
250
448
|
|
|
251
|
-
for bitstring, prob in
|
|
449
|
+
for bitstring, prob in prob_dist_or_counts.items():
|
|
252
450
|
# Extract the bits at the specified indices and form the marginalized bitstring
|
|
253
|
-
marginalized_bitstring = "".join(bitstring[i] for i in indices)
|
|
451
|
+
marginalized_bitstring = "".join(bitstring[i] for i in sorted(indices))
|
|
254
452
|
# Sum up probabilities for each marginalized bitstring
|
|
255
453
|
marginal_dist[marginalized_bitstring] += prob
|
|
256
454
|
|
|
257
455
|
return dict(marginal_dist)
|
|
258
456
|
|
|
259
457
|
|
|
458
|
+
def median_with_uncertainty(observations: Sequence[float]) -> Dict[str, float]:
|
|
459
|
+
"""Computes the median of a Sequence of float observations and returns value and propagated uncertainty.
|
|
460
|
+
Reference: https://mathworld.wolfram.com/StatisticalMedian.html
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
observations (Sequence[float]): a Sequence of floating-point numbers.
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
Dict[str, float]: a dictionary with keys "value" and "uncertainty" for the median of the input Sequence.
|
|
467
|
+
"""
|
|
468
|
+
median = np.median(observations)
|
|
469
|
+
N = len(observations)
|
|
470
|
+
error_from_mean = np.std(observations) / np.sqrt(N)
|
|
471
|
+
median_uncertainty = error_from_mean * np.sqrt(np.pi * N / (2 * (N - 1)))
|
|
472
|
+
|
|
473
|
+
return {"value": float(median), "uncertainty": float(median_uncertainty)}
|
|
474
|
+
|
|
475
|
+
|
|
260
476
|
@timeit
|
|
261
477
|
def perform_backend_transpilation(
|
|
262
478
|
qc_list: List[QuantumCircuit],
|
|
263
479
|
backend: IQMBackendBase,
|
|
264
480
|
qubits: Sequence[int],
|
|
265
481
|
coupling_map: List[List[int]],
|
|
266
|
-
basis_gates:
|
|
482
|
+
basis_gates: Sequence[str] = ("r", "cz"),
|
|
267
483
|
qiskit_optim_level: int = 1,
|
|
268
484
|
optimize_sqg: bool = False,
|
|
269
485
|
drop_final_rz: bool = True,
|
|
@@ -301,21 +517,22 @@ def perform_backend_transpilation(
|
|
|
301
517
|
initial_layout=qubits if aux_qc is None else None,
|
|
302
518
|
routing_method=routing_method,
|
|
303
519
|
)
|
|
304
|
-
if
|
|
305
|
-
transpiled = optimize_single_qubit_gates(transpiled, drop_final_rz=drop_final_rz)
|
|
306
|
-
if "move" in backend.operation_names:
|
|
520
|
+
if "move" in backend.architecture.gates:
|
|
307
521
|
transpiled = transpile_to_IQM(
|
|
308
522
|
qc, backend=backend, optimize_single_qubits=optimize_sqg, remove_final_rzs=drop_final_rz
|
|
309
523
|
)
|
|
310
524
|
if aux_qc is not None:
|
|
311
|
-
if "move" in backend.
|
|
312
|
-
if
|
|
525
|
+
if "move" in backend.architecture.gates:
|
|
526
|
+
if backend.num_qubits in qubits:
|
|
313
527
|
raise ValueError(
|
|
314
|
-
"Label
|
|
528
|
+
f"Label {backend.num_qubits} is reserved for Resonator - "
|
|
529
|
+
f"Please specify computational qubit labels {np.arange(backend.num_qubits)}"
|
|
315
530
|
)
|
|
316
|
-
|
|
317
|
-
transpiled = reduce_to_active_qubits(transpiled,
|
|
318
|
-
transpiled = aux_qc.compose(
|
|
531
|
+
backend_topology = "star"
|
|
532
|
+
transpiled = reduce_to_active_qubits(transpiled, backend_topology, backend.num_qubits)
|
|
533
|
+
transpiled = aux_qc.compose(
|
|
534
|
+
transpiled, qubits=qubits + [backend.num_qubits], clbits=list(range(qc.num_clbits))
|
|
535
|
+
)
|
|
319
536
|
else:
|
|
320
537
|
transpiled = aux_qc.compose(transpiled, qubits=qubits, clbits=list(range(qc.num_clbits)))
|
|
321
538
|
|
|
@@ -323,40 +540,46 @@ def perform_backend_transpilation(
|
|
|
323
540
|
|
|
324
541
|
qcvv_logger.info(
|
|
325
542
|
f"Transpiling for backend {backend.name} with optimization level {qiskit_optim_level}, "
|
|
326
|
-
f"{routing_method} routing method{'
|
|
543
|
+
f"{routing_method} routing method{' including SQG optimization' if qiskit_optim_level>0 else ''} all circuits"
|
|
327
544
|
)
|
|
328
545
|
|
|
329
546
|
if coupling_map == backend.coupling_map:
|
|
330
547
|
transpiled_qc_list = [transpile_and_optimize(qc) for qc in qc_list]
|
|
331
548
|
else: # The coupling map will be reduced if the physical layout is to be fixed
|
|
332
|
-
|
|
549
|
+
if "move" in backend.architecture.gates:
|
|
550
|
+
aux_qc_list = [QuantumCircuit(backend.num_qubits + 1, q.num_clbits) for q in qc_list]
|
|
551
|
+
else:
|
|
552
|
+
aux_qc_list = [QuantumCircuit(backend.num_qubits, q.num_clbits) for q in qc_list]
|
|
333
553
|
transpiled_qc_list = [transpile_and_optimize(qc, aux_qc=aux_qc_list[idx]) for idx, qc in enumerate(qc_list)]
|
|
334
554
|
|
|
335
555
|
return transpiled_qc_list
|
|
336
556
|
|
|
337
557
|
|
|
338
|
-
def reduce_to_active_qubits(
|
|
558
|
+
def reduce_to_active_qubits(
|
|
559
|
+
circuit: QuantumCircuit, backend_topology: Optional[str] = None, backend_num_qubits=None
|
|
560
|
+
) -> QuantumCircuit:
|
|
339
561
|
"""
|
|
340
562
|
Reduces a quantum circuit to only its active qubits.
|
|
341
563
|
|
|
342
564
|
Args:
|
|
343
|
-
|
|
565
|
+
backend_topology (Optional[str]): The backend topology to execute the benchmark on.
|
|
344
566
|
circuit (QuantumCircuit): The original quantum circuit.
|
|
567
|
+
backend_num_qubits (int): The number of qubits in the backend.
|
|
345
568
|
|
|
346
569
|
Returns:
|
|
347
570
|
QuantumCircuit: A new quantum circuit containing only active qubits.
|
|
348
571
|
"""
|
|
349
572
|
# Identify active qubits
|
|
350
|
-
active_qubits = set()
|
|
573
|
+
active_qubits: list | set = set()
|
|
351
574
|
for instruction in circuit.data:
|
|
352
575
|
for qubit in instruction.qubits:
|
|
353
|
-
active_qubits.add(circuit.find_bit(qubit).index)
|
|
354
|
-
if
|
|
576
|
+
cast(set, active_qubits).add(circuit.find_bit(qubit).index)
|
|
577
|
+
if backend_topology == "star" and backend_num_qubits not in active_qubits:
|
|
355
578
|
# For star systems, the resonator must always be there, regardless of whether it MOVE gates on it or not
|
|
356
|
-
active_qubits.add(
|
|
579
|
+
cast(set, active_qubits).add(backend_num_qubits)
|
|
357
580
|
|
|
358
581
|
# Create a mapping from old qubits to new qubits
|
|
359
|
-
active_qubits = set(sorted(active_qubits))
|
|
582
|
+
active_qubits = list(set(sorted(active_qubits)))
|
|
360
583
|
qubit_map = {old_idx: new_idx for new_idx, old_idx in enumerate(active_qubits)}
|
|
361
584
|
|
|
362
585
|
# Create a new quantum circuit with the reduced number of qubits
|
|
@@ -376,6 +599,18 @@ def reduce_to_active_qubits(circuit: QuantumCircuit, backend_name: Optional[str]
|
|
|
376
599
|
return reduced_circuit
|
|
377
600
|
|
|
378
601
|
|
|
602
|
+
def remove_directed_duplicates_to_list(cp_map: CouplingMap) -> List[List[int]]:
|
|
603
|
+
"""Remove duplicate edges from a coupling map and returns as a list of edges (as a list of pairs of vertices).
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
cp_map (CouplingMap): A list of pairs of integers, representing a coupling map.
|
|
607
|
+
Returns:
|
|
608
|
+
List[List[int]]: the edges of the coupling map.
|
|
609
|
+
"""
|
|
610
|
+
sorted_cp = [sorted(x) for x in list(cp_map)]
|
|
611
|
+
return [list(x) for x in set(map(tuple, sorted_cp))]
|
|
612
|
+
|
|
613
|
+
|
|
379
614
|
@timeit
|
|
380
615
|
def retrieve_all_counts(iqm_jobs: List[IQMJob], identifier: Optional[str] = None) -> List[Dict[str, int]]:
|
|
381
616
|
"""Retrieve the counts from a list of IQMJob objects.
|
|
@@ -445,6 +680,8 @@ def set_coupling_map(
|
|
|
445
680
|
- "fixed" sets a coupling map restricted to the input qubits -> results will be constrained to measure those qubits.
|
|
446
681
|
- "batching" sets the coupling map of the backend -> results in a benchmark will be "batched" according to final layouts.
|
|
447
682
|
* Default is "fixed".
|
|
683
|
+
Raises:
|
|
684
|
+
ValueError: if the physical layout is not "fixed" or "batching".
|
|
448
685
|
Returns:
|
|
449
686
|
A coupling map according to the specified physical layout.
|
|
450
687
|
|
|
@@ -453,18 +690,41 @@ def set_coupling_map(
|
|
|
453
690
|
ValueError: if the physical layout is not "fixed" or "batching".
|
|
454
691
|
"""
|
|
455
692
|
if physical_layout == "fixed":
|
|
456
|
-
if "move" in backend.
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
693
|
+
# if "move" in backend.architecture.gates:
|
|
694
|
+
# if 0 in qubits:
|
|
695
|
+
# raise ValueError(
|
|
696
|
+
# "Label 0 is reserved for Resonator - Please specify computational qubit labels (1,2,...)"
|
|
697
|
+
# )
|
|
698
|
+
# return backend.coupling_map.reduce(mapping=[0] + list(qubits))
|
|
462
699
|
return backend.coupling_map.reduce(mapping=qubits)
|
|
463
700
|
if physical_layout == "batching":
|
|
464
701
|
return backend.coupling_map
|
|
465
702
|
raise ValueError('physical_layout must either be "fixed" or "batching"')
|
|
466
703
|
|
|
467
704
|
|
|
705
|
+
def split_sequence_in_chunks(sequence_in: Sequence[Any], split_size: int) -> List[Sequence[Any]]:
|
|
706
|
+
"""Split a given Sequence into chunks of a given split size, return as a List of Sequences.
|
|
707
|
+
|
|
708
|
+
Args:
|
|
709
|
+
sequence_in (Sequence[Any]): The input list.
|
|
710
|
+
split_size (int): The split size.
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
List[Sequence[Any]]: A List of Sequences.
|
|
714
|
+
"""
|
|
715
|
+
if split_size > len(sequence_in):
|
|
716
|
+
raise ValueError("The split size should be smaller or equal than the list length")
|
|
717
|
+
if len(sequence_in) % split_size != 0 and (split_size != 1 and split_size != len(sequence_in)):
|
|
718
|
+
qcvv_logger.debug(
|
|
719
|
+
f"Since len(input_list) = {len(sequence_in)} and split_size = {split_size}, the input list will be split into chunks of uneven size!"
|
|
720
|
+
)
|
|
721
|
+
warnings.warn(
|
|
722
|
+
f"Since len(input_list) = {len(sequence_in)} and split_size = {split_size}, the input list will be split into chunks of uneven size!"
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
return [sequence_in[i : i + split_size] for i in range(0, len(sequence_in), split_size)]
|
|
726
|
+
|
|
727
|
+
|
|
468
728
|
@timeit
|
|
469
729
|
def sort_batches_by_final_layout(
|
|
470
730
|
transpiled_circuit_list: List[QuantumCircuit],
|
|
@@ -498,7 +758,7 @@ def sort_batches_by_final_layout(
|
|
|
498
758
|
|
|
499
759
|
@timeit
|
|
500
760
|
def submit_execute(
|
|
501
|
-
sorted_transpiled_qc_list: Dict[Tuple, List[QuantumCircuit]],
|
|
761
|
+
sorted_transpiled_qc_list: Dict[Tuple[int] | str, List[QuantumCircuit]],
|
|
502
762
|
backend: IQMBackendBase,
|
|
503
763
|
shots: int,
|
|
504
764
|
calset_id: Optional[str] = None,
|
|
@@ -506,10 +766,14 @@ def submit_execute(
|
|
|
506
766
|
max_circuits_per_batch: Optional[int] = None,
|
|
507
767
|
circuit_compilation_options: Optional[CircuitCompilationOptions] = None,
|
|
508
768
|
) -> List[IQMJob]:
|
|
509
|
-
"""Submit
|
|
769
|
+
"""Submit function to execute lists of quantum circuits on the specified backend,
|
|
770
|
+
organized as a dictionary with keys being identifiers of a batch (normally qubits) and values corresponding lists of quantum circuits.
|
|
771
|
+
The result is returned as a single list of IQMJob objects.
|
|
510
772
|
|
|
511
773
|
Args:
|
|
512
|
-
sorted_transpiled_qc_list (Dict[Tuple, List[QuantumCircuit]]):
|
|
774
|
+
sorted_transpiled_qc_list (Dict[Tuple[int] | str, List[QuantumCircuit]]): A dictionary of lists of quantum circuits to be executed.
|
|
775
|
+
* The keys (Tuple[int] | str) should correspond to final measured qubits.
|
|
776
|
+
* The values (List[QuantumCircuit]) should be the corresponding list (batch) of quantum circuits.
|
|
513
777
|
backend (IQMBackendBase): the backend to execute the circuits on.
|
|
514
778
|
shots (int): the number of shots per circuit.
|
|
515
779
|
calset_id (Optional[str]): the calibration set ID.
|
|
@@ -522,8 +786,7 @@ def submit_execute(
|
|
|
522
786
|
enabling execution with dynamical decoupling, among other options - see qiskit-iqm documentation.
|
|
523
787
|
* Default is None.
|
|
524
788
|
Returns:
|
|
525
|
-
List[IQMJob]:
|
|
526
|
-
|
|
789
|
+
List[IQMJob]: a list of IQMJob objects corresponding to the submitted circuits.
|
|
527
790
|
"""
|
|
528
791
|
final_jobs = []
|
|
529
792
|
for k in sorted(
|
|
@@ -600,97 +863,6 @@ def xrvariable_to_counts(dataset: xr.Dataset, identifier: str, counts_range: int
|
|
|
600
863
|
]
|
|
601
864
|
|
|
602
865
|
|
|
603
|
-
@dataclass
|
|
604
|
-
class GraphPositions:
|
|
605
|
-
"""A class to store and generate graph positions for different chip layouts.
|
|
606
|
-
|
|
607
|
-
This class contains predefined node positions for various quantum chip topologies and
|
|
608
|
-
provides methods to generate positions for different layout types.
|
|
609
|
-
|
|
610
|
-
Attributes:
|
|
611
|
-
garnet_positions (Dict[int, Tuple[int, int]]): Mapping of node indices to (x,y) positions for Garnet chip.
|
|
612
|
-
deneb_positions (Dict[int, Tuple[int, int]]): Mapping of node indices to (x,y) positions for Deneb chip.
|
|
613
|
-
predefined_stations (Dict[str, Dict[int, Tuple[int, int]]]): Mapping of chip names to their position dictionaries.
|
|
614
|
-
"""
|
|
615
|
-
|
|
616
|
-
garnet_positions = {
|
|
617
|
-
0: (5.0, 7.0),
|
|
618
|
-
1: (6.0, 6.0),
|
|
619
|
-
2: (3.0, 7.0),
|
|
620
|
-
3: (4.0, 6.0),
|
|
621
|
-
4: (5.0, 5.0),
|
|
622
|
-
5: (6.0, 4.0),
|
|
623
|
-
6: (7.0, 3.0),
|
|
624
|
-
7: (2.0, 6.0),
|
|
625
|
-
8: (3.0, 5.0),
|
|
626
|
-
9: (4.0, 4.0),
|
|
627
|
-
10: (5.0, 3.0),
|
|
628
|
-
11: (6.0, 2.0),
|
|
629
|
-
12: (1.0, 5.0),
|
|
630
|
-
13: (2.0, 4.0),
|
|
631
|
-
14: (3.0, 3.0),
|
|
632
|
-
15: (4.0, 2.0),
|
|
633
|
-
16: (5.0, 1.0),
|
|
634
|
-
17: (1.0, 3.0),
|
|
635
|
-
18: (2.0, 2.0),
|
|
636
|
-
19: (3.0, 1.0),
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
deneb_positions = {
|
|
640
|
-
0: (2.0, 2.0),
|
|
641
|
-
1: (1.0, 1.0),
|
|
642
|
-
3: (2.0, 1.0),
|
|
643
|
-
5: (3.0, 1.0),
|
|
644
|
-
2: (1.0, 3.0),
|
|
645
|
-
4: (2.0, 3.0),
|
|
646
|
-
6: (3.0, 3.0),
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
predefined_stations = {
|
|
650
|
-
"Garnet": garnet_positions,
|
|
651
|
-
"Deneb": deneb_positions,
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
@staticmethod
|
|
655
|
-
def create_positions(graph: PyGraph, topology: Optional[str] = None) -> Dict[int, Tuple[float, float]]:
|
|
656
|
-
"""Generate node positions for a given graph and topology.
|
|
657
|
-
|
|
658
|
-
Args:
|
|
659
|
-
graph: The graph to generate positions for.
|
|
660
|
-
topology: The type of layout to generate. Must be either "star" or "crystal".
|
|
661
|
-
|
|
662
|
-
Returns:
|
|
663
|
-
A dictionary mapping node indices to (x,y) coordinates.
|
|
664
|
-
"""
|
|
665
|
-
n_nodes = len(graph.node_indices())
|
|
666
|
-
|
|
667
|
-
if topology == "star":
|
|
668
|
-
# Place center node at (0,0)
|
|
669
|
-
pos = {0: (0.0, 0.0)}
|
|
670
|
-
|
|
671
|
-
if n_nodes > 1:
|
|
672
|
-
# Place other nodes in a circle around the center
|
|
673
|
-
angles = np.linspace(0, 2 * np.pi, n_nodes - 1, endpoint=False)
|
|
674
|
-
radius = 1.0
|
|
675
|
-
|
|
676
|
-
for i, angle in enumerate(angles, start=1):
|
|
677
|
-
x = radius * np.cos(angle)
|
|
678
|
-
y = radius * np.sin(angle)
|
|
679
|
-
pos[i] = (x, y)
|
|
680
|
-
|
|
681
|
-
# Crystal and other topologies
|
|
682
|
-
else:
|
|
683
|
-
# Fix first node position in bottom right
|
|
684
|
-
fixed_pos = {0: (1.0, 1.0)} # For more consistent layouts
|
|
685
|
-
|
|
686
|
-
# Get spring layout with one fixed position
|
|
687
|
-
pos = {
|
|
688
|
-
int(k): (float(v[0]), float(v[1]))
|
|
689
|
-
for k, v in spring_layout(graph, scale=2, pos=fixed_pos, num_iter=300, fixed={0}).items()
|
|
690
|
-
}
|
|
691
|
-
return pos
|
|
692
|
-
|
|
693
|
-
|
|
694
866
|
def extract_fidelities(cal_url: str) -> tuple[list[list[int]], list[float], str]:
|
|
695
867
|
"""Returns couplings and CZ-fidelities from calibration data URL
|
|
696
868
|
|
|
@@ -721,110 +893,15 @@ def extract_fidelities(cal_url: str) -> tuple[list[list[int]], list[float], str]
|
|
|
721
893
|
i, j = cal_keys["cz_gate_fidelity"]
|
|
722
894
|
topology = "crystal"
|
|
723
895
|
for item in calibration["calibrations"][i]["metrics"][j]["metrics"]:
|
|
724
|
-
qb1 = int(item["locus"][0][2:]) if item["locus"][0]
|
|
725
|
-
qb2 = int(item["locus"][1][2:]) if item["locus"][1]
|
|
726
|
-
|
|
727
|
-
list_couplings.append([qb1, qb2])
|
|
728
|
-
else:
|
|
729
|
-
list_couplings.append([qb1 - 1, qb2 - 1])
|
|
896
|
+
qb1 = int(item["locus"][0][2:]) if "COMP" not in item["locus"][0] else 0
|
|
897
|
+
qb2 = int(item["locus"][1][2:]) if "COMP" not in item["locus"][1] else 0
|
|
898
|
+
list_couplings.append([qb1 - 1, qb2 - 1])
|
|
730
899
|
list_fids.append(float(item["value"]))
|
|
731
900
|
calibrated_qubits = set(np.array(list_couplings).reshape(-1))
|
|
732
|
-
qubit_mapping = {
|
|
901
|
+
qubit_mapping = {}
|
|
902
|
+
if topology == "star":
|
|
903
|
+
qubit_mapping.update({-1: len(calibrated_qubits)}) # Place resonator qubit as last qubit
|
|
904
|
+
qubit_mapping.update({qubit: idx for idx, qubit in enumerate(calibrated_qubits)})
|
|
733
905
|
list_couplings = [[qubit_mapping[edge[0]], qubit_mapping[edge[1]]] for edge in list_couplings]
|
|
734
906
|
|
|
735
907
|
return list_couplings, list_fids, topology
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
def plot_layout_fidelity_graph(
|
|
739
|
-
cal_url: str, qubit_layouts: Optional[list[list[int]]] = None, station: Optional[str] = None
|
|
740
|
-
):
|
|
741
|
-
"""Plot a graph showing the quantum chip layout with fidelity information.
|
|
742
|
-
|
|
743
|
-
Creates a visualization of the quantum chip topology where nodes represent qubits
|
|
744
|
-
and edges represent connections between qubits. Edge thickness indicates gate errors
|
|
745
|
-
(thinner edges mean better fidelity) and selected qubits are highlighted in orange.
|
|
746
|
-
|
|
747
|
-
Args:
|
|
748
|
-
cal_url: URL to retrieve calibration data from
|
|
749
|
-
qubit_layouts: List of qubit layouts where each layout is a list of qubit indices
|
|
750
|
-
station: Name of the quantum computing station to use predefined positions for.
|
|
751
|
-
If None, positions will be generated algorithmically.
|
|
752
|
-
|
|
753
|
-
Returns:
|
|
754
|
-
matplotlib.figure.Figure: The generated figure object containing the graph visualization
|
|
755
|
-
"""
|
|
756
|
-
edges_cal, fidelities_cal, topology = extract_fidelities(cal_url)
|
|
757
|
-
weights = -np.log(np.array(fidelities_cal))
|
|
758
|
-
edges_graph = [tuple(edge) + (weight,) for edge, weight in zip(edges_cal, weights)]
|
|
759
|
-
|
|
760
|
-
graph = PyGraph()
|
|
761
|
-
|
|
762
|
-
# Add nodes
|
|
763
|
-
nodes: set[int] = set()
|
|
764
|
-
for edge in edges_graph:
|
|
765
|
-
nodes.update(edge[:2])
|
|
766
|
-
graph.add_nodes_from(list(nodes))
|
|
767
|
-
|
|
768
|
-
# Add edges
|
|
769
|
-
graph.add_edges_from(edges_graph)
|
|
770
|
-
|
|
771
|
-
# Define qubit positions in plot
|
|
772
|
-
if station in GraphPositions.predefined_stations:
|
|
773
|
-
pos = GraphPositions.predefined_stations[station]
|
|
774
|
-
else:
|
|
775
|
-
pos = GraphPositions.create_positions(graph, topology)
|
|
776
|
-
|
|
777
|
-
# Define node colors
|
|
778
|
-
node_colors = ["lightgrey" for _ in range(len(nodes))]
|
|
779
|
-
if qubit_layouts is not None:
|
|
780
|
-
for qb in {qb for layout in qubit_layouts for qb in layout}:
|
|
781
|
-
node_colors[qb] = "orange"
|
|
782
|
-
|
|
783
|
-
# Ensuring weights are in correct order for the plot
|
|
784
|
-
edge_list = graph.edge_list()
|
|
785
|
-
weights_dict = {}
|
|
786
|
-
edge_pos = set()
|
|
787
|
-
|
|
788
|
-
# Create a mapping between edge positions as defined in rustworkx and their weights
|
|
789
|
-
for e, w in zip(edge_list, weights):
|
|
790
|
-
pos_tuple = (tuple(pos[e[0]]), tuple(pos[e[1]]))
|
|
791
|
-
weights_dict[pos_tuple] = w
|
|
792
|
-
edge_pos.add(pos_tuple)
|
|
793
|
-
|
|
794
|
-
# Get corresponding weights in the same order
|
|
795
|
-
weights_ordered = np.array([weights_dict[edge] for edge in list(edge_pos)])
|
|
796
|
-
|
|
797
|
-
plt.subplots(figsize=(6, 6))
|
|
798
|
-
|
|
799
|
-
# Draw the graph
|
|
800
|
-
visualization.mpl_draw(
|
|
801
|
-
graph,
|
|
802
|
-
with_labels=True,
|
|
803
|
-
node_color=node_colors,
|
|
804
|
-
pos=pos,
|
|
805
|
-
labels=lambda node: node,
|
|
806
|
-
width=7 * weights_ordered / np.max(weights_ordered),
|
|
807
|
-
) # type: ignore[call-arg]
|
|
808
|
-
|
|
809
|
-
# Add edge labels using matplotlib's annotate
|
|
810
|
-
for edge in edges_graph:
|
|
811
|
-
x1, y1 = pos[edge[0]]
|
|
812
|
-
x2, y2 = pos[edge[1]]
|
|
813
|
-
x = (x1 + x2) / 2
|
|
814
|
-
y = (y1 + y2) / 2
|
|
815
|
-
plt.annotate(
|
|
816
|
-
f"{edge[2]:.1e}",
|
|
817
|
-
xy=(x, y),
|
|
818
|
-
xytext=(0, 0),
|
|
819
|
-
textcoords="offset points",
|
|
820
|
-
ha="center",
|
|
821
|
-
va="center",
|
|
822
|
-
bbox={"boxstyle": "round,pad=0.2", "fc": "white", "ec": "none", "alpha": 0.6},
|
|
823
|
-
)
|
|
824
|
-
|
|
825
|
-
plt.gca().invert_yaxis()
|
|
826
|
-
plt.title(
|
|
827
|
-
"Chip layout with selected qubits in orange\n"
|
|
828
|
-
+ "and gate errors indicated by edge thickness (thinner is better)"
|
|
829
|
-
)
|
|
830
|
-
plt.show()
|