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

@@ -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]]): qubits to submit jobs to.
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() -> Tuple[Dict[str, QuantumCircuit], Dict[str, QuantumCircuit]]:
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
- Dictionaries of 1Q and 2Q Clifford gates
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
- # Import the native-gate Cliffords
389
- with open(os.path.join(os.path.dirname(__file__), "clifford_1q.pkl"), "rb") as f1q:
390
- clifford_1q_dict = pickle.load(f1q)
391
- with open(os.path.join(os.path.dirname(__file__), "clifford_2q.pkl"), "rb") as f2q:
392
- clifford_2q_dict = pickle.load(f2q)
393
- qcvv_logger.info("Clifford dictionaries imported successfully !")
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 marginal_distribution(prob_dist: Dict[str, float], indices: Iterable[int]) -> Dict[str, float]:
242
- """Compute the marginal distribution over specified bits (indices)
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 (dict): A dictionary with keys being bitstrings and values are their probabilities
246
- - indices (list): List of bit indices to marginalize over
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 prob_dist.items():
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: Tuple[str, ...] = ("r", "cz"),
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 for execute a list of quantum circuits on the specified Backend.
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]]): the list of quantum circuits to be executed.
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]: the IQMJob objects of the executed circuits.
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()