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/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 marginal_distribution(prob_dist: Dict[str, float], indices: Iterable[int]) -> Dict[str, float]:
240
- """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).
241
439
 
242
440
  Params:
243
- - prob_dist (dict): A dictionary with keys being bitstrings and values are their probabilities
244
- - 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
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 prob_dist.items():
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: Tuple[str, ...] = ("r", "cz"),
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 optimize_sqg:
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.operation_names:
312
- if 0 in qubits:
525
+ if "move" in backend.architecture.gates:
526
+ if backend.num_qubits in qubits:
313
527
  raise ValueError(
314
- "Label 0 is reserved for Resonator - Please specify computational qubit labels (1,2,...)"
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
- backend_name = "IQMNdonisBackend"
317
- transpiled = reduce_to_active_qubits(transpiled, backend_name)
318
- transpiled = aux_qc.compose(transpiled, qubits=[0] + qubits, clbits=list(range(qc.num_clbits)))
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{' and SQG optimization' if optimize_sqg else ''} all circuits"
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
- aux_qc_list = [QuantumCircuit(backend.num_qubits, q.num_clbits) for q in qc_list]
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(circuit: QuantumCircuit, backend_name: Optional[str] = None) -> QuantumCircuit:
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
- backend_name (Optional[str]): The backend name, if any, in which the circuits are defined.
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 backend_name is not None and backend_name == "IQMNdonisBackend" and 0 not in active_qubits:
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(0)
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.operation_names:
457
- if 0 in qubits:
458
- raise ValueError(
459
- "Label 0 is reserved for Resonator - Please specify computational qubit labels (1,2,...)"
460
- )
461
- return backend.coupling_map.reduce(mapping=[0] + list(qubits))
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 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.
510
772
 
511
773
  Args:
512
- 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.
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]: the IQMJob objects of the executed circuits.
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] != "COMP_R" else 0
725
- qb2 = int(item["locus"][1][2:]) if item["locus"][1] != "COMP_R" else 0
726
- if topology == "star":
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 = {qubit: idx for idx, qubit in enumerate(calibrated_qubits)}
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()