iqm-benchmarks 2.27__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/entanglement/__init__.py +2 -1
- iqm/benchmarks/entanglement/graph_states.py +1348 -0
- iqm/benchmarks/quantum_volume/quantum_volume.py +3 -6
- iqm/benchmarks/randomized_benchmarking/randomized_benchmarking_common.py +33 -8
- iqm/benchmarks/utils.py +276 -194
- iqm/benchmarks/utils_plots.py +233 -0
- iqm/benchmarks/utils_shadows.py +228 -0
- {iqm_benchmarks-2.27.dist-info → iqm_benchmarks-2.28.dist-info}/METADATA +2 -1
- {iqm_benchmarks-2.27.dist-info → iqm_benchmarks-2.28.dist-info}/RECORD +13 -10
- {iqm_benchmarks-2.27.dist-info → iqm_benchmarks-2.28.dist-info}/WHEEL +1 -1
- {iqm_benchmarks-2.27.dist-info → iqm_benchmarks-2.28.dist-info}/licenses/LICENSE +0 -0
- {iqm_benchmarks-2.27.dist-info → iqm_benchmarks-2.28.dist-info}/top_level.txt +0 -0
|
@@ -38,10 +38,6 @@ from iqm.benchmarks.benchmark_definition import (
|
|
|
38
38
|
BenchmarkRunResult,
|
|
39
39
|
add_counts_to_dataset,
|
|
40
40
|
)
|
|
41
|
-
|
|
42
|
-
# import iqm.diqe.executors.dynamical_decoupling.dd_high_level as dd
|
|
43
|
-
# from iqm.diqe.executors.dynamical_decoupling.dynamical_decoupling_core import DDStrategy
|
|
44
|
-
# from iqm.diqe.mapomatic import evaluate_costs, get_calibration_fidelities, get_circuit, matching_layouts
|
|
45
41
|
from iqm.benchmarks.circuit_containers import BenchmarkCircuit, CircuitGroup, Circuits
|
|
46
42
|
from iqm.benchmarks.logging_config import qcvv_logger
|
|
47
43
|
from iqm.benchmarks.readout_mitigation import apply_readout_error_mitigation
|
|
@@ -677,12 +673,13 @@ class QuantumVolumeBenchmark(Benchmark):
|
|
|
677
673
|
sorted_transpiled_qc_list: Dict[Tuple[int, ...], List[QuantumCircuit]],
|
|
678
674
|
) -> Dict[str, Any]:
|
|
679
675
|
"""
|
|
680
|
-
Submit jobs for execution in the specified IQMBackend
|
|
676
|
+
Submit a single set of QV jobs for execution in the specified IQMBackend:
|
|
677
|
+
Organizes the results in a dictionary with the qubit layout, the submitted job objects, the type of QV results and submission time.
|
|
681
678
|
|
|
682
679
|
Args:
|
|
683
680
|
backend (IQMBackendBase): the IQM backend to submit the job.
|
|
684
681
|
qubits (List[int]): the qubits to identify the submitted job.
|
|
685
|
-
sorted_transpiled_qc_list (Dict[str, List[QuantumCircuit]]):
|
|
682
|
+
sorted_transpiled_qc_list (Dict[Tuple[int, ...] | str, List[QuantumCircuit]]): A dictionary of Lists of quantum circuits.
|
|
686
683
|
Returns:
|
|
687
684
|
Dict with qubit layout, submitted job objects, type (vanilla/DD) and submission time.
|
|
688
685
|
"""
|
|
@@ -380,17 +380,42 @@ def get_survival_probabilities(num_qubits: int, counts: List[Dict[str, int]]) ->
|
|
|
380
380
|
return [c["0" * num_qubits] / sum(c.values()) if "0" * num_qubits in c.keys() else 0 for c in counts]
|
|
381
381
|
|
|
382
382
|
|
|
383
|
-
def import_native_gate_cliffords(
|
|
383
|
+
def import_native_gate_cliffords(
|
|
384
|
+
system_size: Optional[str] = None,
|
|
385
|
+
) -> Dict[str, QuantumCircuit] | Tuple[Dict[str, QuantumCircuit], Dict[str, QuantumCircuit]]:
|
|
384
386
|
"""Import native gate Clifford dictionaries
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
system_size (str, optional): System size to load, either "1q" or "2q". If None, load both dictionaries.
|
|
390
|
+
|
|
385
391
|
Returns:
|
|
386
|
-
|
|
392
|
+
If system_size is specified, returns the dictionary for that system size.
|
|
393
|
+
If system_size is None, returns a tuple of (1q_dict, 2q_dict).
|
|
394
|
+
|
|
395
|
+
Raises:
|
|
396
|
+
ValueError: If system_size is not None, "1q", or "2q".
|
|
387
397
|
"""
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
398
|
+
if system_size is not None and system_size not in ["1q", "2q"]:
|
|
399
|
+
raise ValueError('system_size must be either "1q", "2q", or None')
|
|
400
|
+
|
|
401
|
+
clifford_1q_dict = {}
|
|
402
|
+
clifford_2q_dict = {}
|
|
403
|
+
|
|
404
|
+
if system_size is None or system_size == "1q":
|
|
405
|
+
with open(os.path.join(os.path.dirname(__file__), "clifford_1q.pkl"), "rb") as f1q:
|
|
406
|
+
clifford_1q_dict = pickle.load(f1q)
|
|
407
|
+
|
|
408
|
+
if system_size is None or system_size == "2q":
|
|
409
|
+
with open(os.path.join(os.path.dirname(__file__), "clifford_2q.pkl"), "rb") as f2q:
|
|
410
|
+
clifford_2q_dict = pickle.load(f2q)
|
|
411
|
+
|
|
412
|
+
qcvv_logger.info(f"Clifford dictionaries for {system_size or 'both systems'} imported successfully!")
|
|
413
|
+
|
|
414
|
+
if system_size == "1q":
|
|
415
|
+
return clifford_1q_dict
|
|
416
|
+
if system_size == "2q":
|
|
417
|
+
return clifford_2q_dict
|
|
418
|
+
|
|
394
419
|
return clifford_1q_dict, clifford_2q_dict
|
|
395
420
|
|
|
396
421
|
|
iqm/benchmarks/utils.py
CHANGED
|
@@ -15,31 +15,30 @@
|
|
|
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
|
|
@@ -196,6 +195,92 @@ def count_native_gates(
|
|
|
196
195
|
return avg_native_operations
|
|
197
196
|
|
|
198
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
|
+
|
|
199
284
|
# pylint: disable=too-many-branches
|
|
200
285
|
def get_iqm_backend(backend_label: str) -> IQMBackendBase:
|
|
201
286
|
"""Get the IQM backend object from a backend name (str).
|
|
@@ -231,6 +316,9 @@ def get_iqm_backend(backend_label: str) -> IQMBackendBase:
|
|
|
231
316
|
iqm_server_url = "https://cocos.resonance.meetiqm.com/deneb"
|
|
232
317
|
provider = IQMProvider(iqm_server_url)
|
|
233
318
|
backend_object = provider.get_backend()
|
|
319
|
+
# FakeDeneb
|
|
320
|
+
elif backend_label.lower() in ("iqmfakedeneb", "fakedeneb"):
|
|
321
|
+
backend_object = IQMFakeDeneb()
|
|
234
322
|
|
|
235
323
|
else:
|
|
236
324
|
raise ValueError(f"Backend {backend_label} not supported. Try 'garnet', 'deneb', 'fakeadonis' or 'fakeapollo'.")
|
|
@@ -238,34 +326,160 @@ def get_iqm_backend(backend_label: str) -> IQMBackendBase:
|
|
|
238
326
|
return backend_object
|
|
239
327
|
|
|
240
328
|
|
|
241
|
-
def
|
|
242
|
-
"""
|
|
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).
|
|
243
439
|
|
|
244
440
|
Params:
|
|
245
|
-
- prob_dist (
|
|
246
|
-
- 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
|
|
247
443
|
|
|
248
444
|
Returns:
|
|
249
445
|
- dict: A dictionary representing the marginal distribution over the specified bits.
|
|
250
446
|
"""
|
|
251
447
|
marginal_dist: Dict[str, float] = defaultdict(float)
|
|
252
448
|
|
|
253
|
-
for bitstring, prob in
|
|
449
|
+
for bitstring, prob in prob_dist_or_counts.items():
|
|
254
450
|
# Extract the bits at the specified indices and form the marginalized bitstring
|
|
255
|
-
marginalized_bitstring = "".join(bitstring[i] for i in indices)
|
|
451
|
+
marginalized_bitstring = "".join(bitstring[i] for i in sorted(indices))
|
|
256
452
|
# Sum up probabilities for each marginalized bitstring
|
|
257
453
|
marginal_dist[marginalized_bitstring] += prob
|
|
258
454
|
|
|
259
455
|
return dict(marginal_dist)
|
|
260
456
|
|
|
261
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
|
+
|
|
262
476
|
@timeit
|
|
263
477
|
def perform_backend_transpilation(
|
|
264
478
|
qc_list: List[QuantumCircuit],
|
|
265
479
|
backend: IQMBackendBase,
|
|
266
480
|
qubits: Sequence[int],
|
|
267
481
|
coupling_map: List[List[int]],
|
|
268
|
-
basis_gates:
|
|
482
|
+
basis_gates: Sequence[str] = ("r", "cz"),
|
|
269
483
|
qiskit_optim_level: int = 1,
|
|
270
484
|
optimize_sqg: bool = False,
|
|
271
485
|
drop_final_rz: bool = True,
|
|
@@ -356,16 +570,16 @@ def reduce_to_active_qubits(
|
|
|
356
570
|
QuantumCircuit: A new quantum circuit containing only active qubits.
|
|
357
571
|
"""
|
|
358
572
|
# Identify active qubits
|
|
359
|
-
active_qubits = set()
|
|
573
|
+
active_qubits: list | set = set()
|
|
360
574
|
for instruction in circuit.data:
|
|
361
575
|
for qubit in instruction.qubits:
|
|
362
|
-
active_qubits.add(circuit.find_bit(qubit).index)
|
|
576
|
+
cast(set, active_qubits).add(circuit.find_bit(qubit).index)
|
|
363
577
|
if backend_topology == "star" and backend_num_qubits not in active_qubits:
|
|
364
578
|
# For star systems, the resonator must always be there, regardless of whether it MOVE gates on it or not
|
|
365
|
-
active_qubits.add(backend_num_qubits)
|
|
579
|
+
cast(set, active_qubits).add(backend_num_qubits)
|
|
366
580
|
|
|
367
581
|
# Create a mapping from old qubits to new qubits
|
|
368
|
-
active_qubits = set(sorted(active_qubits))
|
|
582
|
+
active_qubits = list(set(sorted(active_qubits)))
|
|
369
583
|
qubit_map = {old_idx: new_idx for new_idx, old_idx in enumerate(active_qubits)}
|
|
370
584
|
|
|
371
585
|
# Create a new quantum circuit with the reduced number of qubits
|
|
@@ -385,6 +599,18 @@ def reduce_to_active_qubits(
|
|
|
385
599
|
return reduced_circuit
|
|
386
600
|
|
|
387
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
|
+
|
|
388
614
|
@timeit
|
|
389
615
|
def retrieve_all_counts(iqm_jobs: List[IQMJob], identifier: Optional[str] = None) -> List[Dict[str, int]]:
|
|
390
616
|
"""Retrieve the counts from a list of IQMJob objects.
|
|
@@ -454,6 +680,8 @@ def set_coupling_map(
|
|
|
454
680
|
- "fixed" sets a coupling map restricted to the input qubits -> results will be constrained to measure those qubits.
|
|
455
681
|
- "batching" sets the coupling map of the backend -> results in a benchmark will be "batched" according to final layouts.
|
|
456
682
|
* Default is "fixed".
|
|
683
|
+
Raises:
|
|
684
|
+
ValueError: if the physical layout is not "fixed" or "batching".
|
|
457
685
|
Returns:
|
|
458
686
|
A coupling map according to the specified physical layout.
|
|
459
687
|
|
|
@@ -474,6 +702,29 @@ def set_coupling_map(
|
|
|
474
702
|
raise ValueError('physical_layout must either be "fixed" or "batching"')
|
|
475
703
|
|
|
476
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
|
+
|
|
477
728
|
@timeit
|
|
478
729
|
def sort_batches_by_final_layout(
|
|
479
730
|
transpiled_circuit_list: List[QuantumCircuit],
|
|
@@ -507,7 +758,7 @@ def sort_batches_by_final_layout(
|
|
|
507
758
|
|
|
508
759
|
@timeit
|
|
509
760
|
def submit_execute(
|
|
510
|
-
sorted_transpiled_qc_list: Dict[Tuple, List[QuantumCircuit]],
|
|
761
|
+
sorted_transpiled_qc_list: Dict[Tuple[int] | str, List[QuantumCircuit]],
|
|
511
762
|
backend: IQMBackendBase,
|
|
512
763
|
shots: int,
|
|
513
764
|
calset_id: Optional[str] = None,
|
|
@@ -515,10 +766,14 @@ def submit_execute(
|
|
|
515
766
|
max_circuits_per_batch: Optional[int] = None,
|
|
516
767
|
circuit_compilation_options: Optional[CircuitCompilationOptions] = None,
|
|
517
768
|
) -> List[IQMJob]:
|
|
518
|
-
"""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.
|
|
519
772
|
|
|
520
773
|
Args:
|
|
521
|
-
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.
|
|
522
777
|
backend (IQMBackendBase): the backend to execute the circuits on.
|
|
523
778
|
shots (int): the number of shots per circuit.
|
|
524
779
|
calset_id (Optional[str]): the calibration set ID.
|
|
@@ -531,8 +786,7 @@ def submit_execute(
|
|
|
531
786
|
enabling execution with dynamical decoupling, among other options - see qiskit-iqm documentation.
|
|
532
787
|
* Default is None.
|
|
533
788
|
Returns:
|
|
534
|
-
List[IQMJob]:
|
|
535
|
-
|
|
789
|
+
List[IQMJob]: a list of IQMJob objects corresponding to the submitted circuits.
|
|
536
790
|
"""
|
|
537
791
|
final_jobs = []
|
|
538
792
|
for k in sorted(
|
|
@@ -609,97 +863,6 @@ def xrvariable_to_counts(dataset: xr.Dataset, identifier: str, counts_range: int
|
|
|
609
863
|
]
|
|
610
864
|
|
|
611
865
|
|
|
612
|
-
@dataclass
|
|
613
|
-
class GraphPositions:
|
|
614
|
-
"""A class to store and generate graph positions for different chip layouts.
|
|
615
|
-
|
|
616
|
-
This class contains predefined node positions for various quantum chip topologies and
|
|
617
|
-
provides methods to generate positions for different layout types.
|
|
618
|
-
|
|
619
|
-
Attributes:
|
|
620
|
-
garnet_positions (Dict[int, Tuple[int, int]]): Mapping of node indices to (x,y) positions for Garnet chip.
|
|
621
|
-
deneb_positions (Dict[int, Tuple[int, int]]): Mapping of node indices to (x,y) positions for Deneb chip.
|
|
622
|
-
predefined_stations (Dict[str, Dict[int, Tuple[int, int]]]): Mapping of chip names to their position dictionaries.
|
|
623
|
-
"""
|
|
624
|
-
|
|
625
|
-
garnet_positions = {
|
|
626
|
-
0: (5.0, 7.0),
|
|
627
|
-
1: (6.0, 6.0),
|
|
628
|
-
2: (3.0, 7.0),
|
|
629
|
-
3: (4.0, 6.0),
|
|
630
|
-
4: (5.0, 5.0),
|
|
631
|
-
5: (6.0, 4.0),
|
|
632
|
-
6: (7.0, 3.0),
|
|
633
|
-
7: (2.0, 6.0),
|
|
634
|
-
8: (3.0, 5.0),
|
|
635
|
-
9: (4.0, 4.0),
|
|
636
|
-
10: (5.0, 3.0),
|
|
637
|
-
11: (6.0, 2.0),
|
|
638
|
-
12: (1.0, 5.0),
|
|
639
|
-
13: (2.0, 4.0),
|
|
640
|
-
14: (3.0, 3.0),
|
|
641
|
-
15: (4.0, 2.0),
|
|
642
|
-
16: (5.0, 1.0),
|
|
643
|
-
17: (1.0, 3.0),
|
|
644
|
-
18: (2.0, 2.0),
|
|
645
|
-
19: (3.0, 1.0),
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
deneb_positions = {
|
|
649
|
-
6: (2.0, 2.0),
|
|
650
|
-
0: (1.0, 1.0),
|
|
651
|
-
1: (2.0, 1.0),
|
|
652
|
-
2: (3.0, 1.0),
|
|
653
|
-
3: (1.0, 3.0),
|
|
654
|
-
4: (2.0, 3.0),
|
|
655
|
-
5: (3.0, 3.0),
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
predefined_stations = {
|
|
659
|
-
"Garnet": garnet_positions,
|
|
660
|
-
"Deneb": deneb_positions,
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
@staticmethod
|
|
664
|
-
def create_positions(graph: PyGraph, topology: Optional[str] = None) -> Dict[int, Tuple[float, float]]:
|
|
665
|
-
"""Generate node positions for a given graph and topology.
|
|
666
|
-
|
|
667
|
-
Args:
|
|
668
|
-
graph: The graph to generate positions for.
|
|
669
|
-
topology: The type of layout to generate. Must be either "star" or "crystal".
|
|
670
|
-
|
|
671
|
-
Returns:
|
|
672
|
-
A dictionary mapping node indices to (x,y) coordinates.
|
|
673
|
-
"""
|
|
674
|
-
n_nodes = len(graph.node_indices())
|
|
675
|
-
|
|
676
|
-
if topology == "star":
|
|
677
|
-
# Place resonator node with index n_nodes-1 at (0,0)
|
|
678
|
-
pos = {n_nodes - 1: (0.0, 0.0)}
|
|
679
|
-
|
|
680
|
-
if n_nodes > 1:
|
|
681
|
-
# Place other nodes in a circle around the center
|
|
682
|
-
angles = np.linspace(0, 2 * np.pi, n_nodes - 1, endpoint=False)
|
|
683
|
-
radius = 1.0
|
|
684
|
-
|
|
685
|
-
for i, angle in enumerate(angles):
|
|
686
|
-
x = radius * np.cos(angle)
|
|
687
|
-
y = radius * np.sin(angle)
|
|
688
|
-
pos[i] = (x, y)
|
|
689
|
-
|
|
690
|
-
# Crystal and other topologies
|
|
691
|
-
else:
|
|
692
|
-
# Fix first node position in bottom right
|
|
693
|
-
fixed_pos = {0: (1.0, 1.0)} # For more consistent layouts
|
|
694
|
-
|
|
695
|
-
# Get spring layout with one fixed position
|
|
696
|
-
pos = {
|
|
697
|
-
int(k): (float(v[0]), float(v[1]))
|
|
698
|
-
for k, v in spring_layout(graph, scale=2, pos=fixed_pos, num_iter=500, k=0.15, fixed={0}).items()
|
|
699
|
-
}
|
|
700
|
-
return pos
|
|
701
|
-
|
|
702
|
-
|
|
703
866
|
def extract_fidelities(cal_url: str) -> tuple[list[list[int]], list[float], str]:
|
|
704
867
|
"""Returns couplings and CZ-fidelities from calibration data URL
|
|
705
868
|
|
|
@@ -742,84 +905,3 @@ def extract_fidelities(cal_url: str) -> tuple[list[list[int]], list[float], str]
|
|
|
742
905
|
list_couplings = [[qubit_mapping[edge[0]], qubit_mapping[edge[1]]] for edge in list_couplings]
|
|
743
906
|
|
|
744
907
|
return list_couplings, list_fids, topology
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
def plot_layout_fidelity_graph(cal_url: str, qubit_layouts: Optional[list[list[int]]] = None):
|
|
748
|
-
"""Plot a graph showing the quantum chip layout with fidelity information.
|
|
749
|
-
|
|
750
|
-
Creates a visualization of the quantum chip topology where nodes represent qubits
|
|
751
|
-
and edges represent connections between qubits. Edge thickness indicates gate errors
|
|
752
|
-
(thinner edges mean better fidelity) and selected qubits are highlighted in orange.
|
|
753
|
-
|
|
754
|
-
Args:
|
|
755
|
-
cal_url: URL to retrieve calibration data from
|
|
756
|
-
qubit_layouts: List of qubit layouts where each layout is a list of qubit indices
|
|
757
|
-
|
|
758
|
-
Returns:
|
|
759
|
-
matplotlib.figure.Figure: The generated figure object containing the graph visualization
|
|
760
|
-
"""
|
|
761
|
-
edges_cal, fidelities_cal, topology = extract_fidelities(cal_url)
|
|
762
|
-
weights = -np.log(np.array(fidelities_cal))
|
|
763
|
-
edges_graph = [tuple(edge) + (weight,) for edge, weight in zip(edges_cal, weights)]
|
|
764
|
-
|
|
765
|
-
graph = PyGraph()
|
|
766
|
-
|
|
767
|
-
# Add nodes
|
|
768
|
-
nodes: set[int] = set()
|
|
769
|
-
for edge in edges_graph:
|
|
770
|
-
nodes.update(edge[:2])
|
|
771
|
-
graph.add_nodes_from(list(nodes))
|
|
772
|
-
|
|
773
|
-
# Add edges
|
|
774
|
-
graph.add_edges_from(edges_graph)
|
|
775
|
-
|
|
776
|
-
# Extract station name from URL
|
|
777
|
-
parts = cal_url.strip("/").split("/")
|
|
778
|
-
station = parts[-2].capitalize()
|
|
779
|
-
|
|
780
|
-
# Define qubit positions in plot
|
|
781
|
-
if station in GraphPositions.predefined_stations:
|
|
782
|
-
pos = GraphPositions.predefined_stations[station]
|
|
783
|
-
else:
|
|
784
|
-
pos = GraphPositions.create_positions(graph, topology)
|
|
785
|
-
|
|
786
|
-
# Define node colors
|
|
787
|
-
node_colors = ["lightgrey" for _ in range(len(nodes))]
|
|
788
|
-
if qubit_layouts is not None:
|
|
789
|
-
for qb in {qb for layout in qubit_layouts for qb in layout}:
|
|
790
|
-
node_colors[qb] = "orange"
|
|
791
|
-
|
|
792
|
-
plt.subplots(figsize=(1.5 * np.sqrt(len(nodes)), 1.5 * np.sqrt(len(nodes))))
|
|
793
|
-
|
|
794
|
-
# Draw the graph
|
|
795
|
-
visualization.mpl_draw(
|
|
796
|
-
graph,
|
|
797
|
-
with_labels=True,
|
|
798
|
-
node_color=node_colors,
|
|
799
|
-
pos=pos,
|
|
800
|
-
labels=lambda node: node,
|
|
801
|
-
width=5 * weights / np.max(weights),
|
|
802
|
-
) # type: ignore[call-arg]
|
|
803
|
-
|
|
804
|
-
# Add edge labels using matplotlib's annotate
|
|
805
|
-
for edge in edges_graph:
|
|
806
|
-
x1, y1 = pos[edge[0]]
|
|
807
|
-
x2, y2 = pos[edge[1]]
|
|
808
|
-
x = (x1 + x2) / 2
|
|
809
|
-
y = (y1 + y2) / 2
|
|
810
|
-
plt.annotate(
|
|
811
|
-
f"{edge[2]:.1e}",
|
|
812
|
-
xy=(x, y),
|
|
813
|
-
xytext=(0, 0),
|
|
814
|
-
textcoords="offset points",
|
|
815
|
-
ha="center",
|
|
816
|
-
va="center",
|
|
817
|
-
bbox={"boxstyle": "round,pad=0.2", "fc": "white", "ec": "none", "alpha": 0.6},
|
|
818
|
-
)
|
|
819
|
-
|
|
820
|
-
plt.gca().invert_yaxis()
|
|
821
|
-
plt.title(
|
|
822
|
-
"Chip layout with selected qubits in orange\n"
|
|
823
|
-
+ "and gate errors indicated by edge thickness (thinner is better)"
|
|
824
|
-
)
|
|
825
|
-
plt.show()
|