iqm-client 32.0.0__py3-none-any.whl → 33.0.0__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.
Files changed (45) hide show
  1. iqm/cirq_iqm/devices/iqm_device_metadata.py +2 -1
  2. iqm/cirq_iqm/examples/demo_common.py +1 -1
  3. iqm/cirq_iqm/examples/demo_iqm_execution.py +3 -3
  4. iqm/cirq_iqm/iqm_sampler.py +47 -29
  5. iqm/cirq_iqm/serialize.py +1 -1
  6. iqm/cirq_iqm/transpiler.py +3 -1
  7. iqm/iqm_client/__init__.py +0 -2
  8. iqm/iqm_client/errors.py +6 -17
  9. iqm/iqm_client/iqm_client.py +199 -602
  10. iqm/iqm_client/models.py +20 -611
  11. iqm/iqm_client/transpile.py +11 -8
  12. iqm/iqm_client/validation.py +18 -9
  13. iqm/iqm_server_client/__init__.py +14 -0
  14. iqm/iqm_server_client/errors.py +6 -0
  15. iqm/iqm_server_client/iqm_server_client.py +755 -0
  16. iqm/iqm_server_client/models.py +179 -0
  17. iqm/iqm_server_client/py.typed +0 -0
  18. iqm/qiskit_iqm/__init__.py +8 -0
  19. iqm/qiskit_iqm/examples/bell_measure.py +2 -2
  20. iqm/qiskit_iqm/examples/transpile_example.py +9 -4
  21. iqm/qiskit_iqm/fake_backends/fake_adonis.py +2 -1
  22. iqm/qiskit_iqm/fake_backends/fake_aphrodite.py +2 -1
  23. iqm/qiskit_iqm/fake_backends/fake_apollo.py +2 -1
  24. iqm/qiskit_iqm/fake_backends/fake_deneb.py +2 -1
  25. iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py +8 -7
  26. iqm/qiskit_iqm/iqm_backend.py +3 -4
  27. iqm/qiskit_iqm/iqm_circuit_validation.py +8 -7
  28. iqm/qiskit_iqm/iqm_job.py +106 -88
  29. iqm/qiskit_iqm/iqm_move_layout.py +2 -1
  30. iqm/qiskit_iqm/iqm_naive_move_pass.py +115 -56
  31. iqm/qiskit_iqm/iqm_provider.py +49 -36
  32. iqm/qiskit_iqm/iqm_target.py +12 -8
  33. iqm/qiskit_iqm/iqm_transpilation.py +219 -26
  34. iqm/qiskit_iqm/qiskit_to_iqm.py +150 -41
  35. iqm/qiskit_iqm/transpiler_plugins.py +11 -8
  36. {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/METADATA +4 -14
  37. iqm_client-33.0.0.dist-info/RECORD +63 -0
  38. iqm/iqm_client/api.py +0 -90
  39. iqm/iqm_client/authentication.py +0 -206
  40. iqm_client-32.0.0.dist-info/RECORD +0 -60
  41. {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/AUTHORS.rst +0 -0
  42. {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/LICENSE.txt +0 -0
  43. {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/WHEEL +0 -0
  44. {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/entry_points.txt +0 -0
  45. {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/top_level.txt +0 -0
@@ -21,13 +21,7 @@ from typing import Any
21
21
  from uuid import UUID
22
22
  import warnings
23
23
 
24
- from iqm.iqm_client import (
25
- CircuitBatch,
26
- CircuitCompilationOptions,
27
- CircuitValidationError,
28
- IQMClient,
29
- RunRequest,
30
- )
24
+ from iqm.iqm_client import CircuitCompilationOptions, CircuitValidationError, IQMClient
31
25
  from iqm.iqm_client.util import to_json_dict
32
26
  from iqm.qiskit_iqm import IQMFakeAphrodite, IQMFakeApollo, IQMFakeBackend, IQMFakeDeneb
33
27
  from iqm.qiskit_iqm.fake_backends import IQMFakeAdonis
@@ -39,6 +33,7 @@ from qiskit import QuantumCircuit
39
33
  from qiskit.providers import JobStatus, JobV1, Options
40
34
 
41
35
  from iqm.pulse import Circuit
36
+ from iqm.station_control.interface.models import CircuitBatch, RunRequest
42
37
 
43
38
  try:
44
39
  __version__ = version("qiskit-iqm")
@@ -52,9 +47,9 @@ class IQMBackend(IQMBackendBase):
52
47
  """Backend for executing quantum circuits on IQM quantum computers.
53
48
 
54
49
  Args:
55
- client: Client instance for communicating with an IQM server.
50
+ client: Client instance for communicating with an IQM Server.
56
51
  calibration_set_id: ID of the calibration set the backend will use.
57
- ``None`` means the IQM server will be queried for the current default
52
+ ``None`` means the IQM Server will be queried for the current default
58
53
  calibration set.
59
54
  use_metrics: If True, the backend will query the server for calibration data and related
60
55
  quality metrics, and pass these to the transpilation target(s). The default value is set
@@ -80,7 +75,6 @@ class IQMBackend(IQMBackendBase):
80
75
  super().__init__(architecture, metrics=metrics, **kwargs)
81
76
  self.client: IQMClient = client
82
77
  self._max_circuits: int | None = None
83
- self.name = "IQM Backend"
84
78
  self._calibration_set_id = architecture.calibration_set_id
85
79
 
86
80
  @classmethod
@@ -110,12 +104,16 @@ class IQMBackend(IQMBackendBase):
110
104
  def run(
111
105
  self,
112
106
  run_input: QuantumCircuit | list[QuantumCircuit],
107
+ *,
108
+ use_timeslot: bool = False,
113
109
  **options,
114
110
  ) -> IQMJob:
115
111
  """Run a quantum circuit or a list of quantum circuits on the IQM quantum computer represented by this backend.
116
112
 
117
113
  Args:
118
114
  run_input: The circuits to run.
115
+ use_timeslot: Submits the job to the timeslot queue if set to ``True``. If set to ``False``,
116
+ the job is submitted to the normal on-demand queue.
119
117
  options: Keyword arguments passed on to :meth:`create_run_request`, and documented there.
120
118
 
121
119
  Returns:
@@ -123,8 +121,8 @@ class IQMBackend(IQMBackendBase):
123
121
 
124
122
  """
125
123
  run_request = self.create_run_request(run_input, **options)
126
- job_id = self.client.submit_run_request(run_request)
127
- job = IQMJob(self, str(job_id), shots=run_request.shots)
124
+ circuit_job = self.client.submit_run_request(run_request, use_timeslot=use_timeslot)
125
+ job = IQMJob(self, circuit_job)
128
126
  job.circuit_metadata = [c.metadata if isinstance(c, Circuit) else {} for c in run_request.circuits]
129
127
  return job
130
128
 
@@ -134,7 +132,7 @@ class IQMBackend(IQMBackendBase):
134
132
  shots: int = 1024,
135
133
  circuit_compilation_options: CircuitCompilationOptions | None = None,
136
134
  circuit_callback: Callable[[list[QuantumCircuit]], Any] | None = None,
137
- qubit_mapping: dict[int, str] | None = None,
135
+ qubit_index_to_name: dict[int, str] | None = None,
138
136
  **unknown_options,
139
137
  ) -> RunRequest:
140
138
  """Creates a run request without submitting it for execution.
@@ -161,8 +159,8 @@ class IQMBackend(IQMBackendBase):
161
159
  As a side effect, you can also use this callback to modify the transpiled circuits
162
160
  in-place, just before execution; however, we do not recommend to use it for this
163
161
  purpose.
164
- qubit_mapping: Mapping from qubit indices in the circuit to qubit names on the device. If ``None``,
165
- :attr:`.IQMBackendBase.index_to_qubit_name` will be used.
162
+ qubit_index_to_name: Mapping from qubit indices in the circuit to qubit names on the device.
163
+ If ``None``, :attr:`.IQMBackendBase.index_to_qubit_name` will be used.
166
164
 
167
165
  Returns:
168
166
  The created run request object
@@ -195,7 +193,9 @@ class IQMBackend(IQMBackendBase):
195
193
  if circuit_callback:
196
194
  circuit_callback(circuits)
197
195
 
198
- circuits_serialized: CircuitBatch = [self.serialize_circuit(circuit, qubit_mapping) for circuit in circuits]
196
+ circuits_serialized: CircuitBatch = [
197
+ self.serialize_circuit(circuit, qubit_index_to_name) for circuit in circuits
198
+ ]
199
199
 
200
200
  if self._use_default_calibration_set:
201
201
  default_calset_id = self.client.get_dynamic_quantum_architecture(None).calibration_set_id
@@ -230,13 +230,10 @@ class IQMBackend(IQMBackendBase):
230
230
  corresponding job
231
231
 
232
232
  """
233
- return IQMJob(self, job_id)
234
-
235
- def close_client(self) -> None:
236
- """Close IQMClient's session with the authentication server."""
237
- self.client.close_auth_session()
233
+ circuit_job = self.client.get_job(UUID(job_id))
234
+ return IQMJob(self, circuit_job)
238
235
 
239
- def serialize_circuit(self, circuit: QuantumCircuit, qubit_mapping: dict[int, str] | None = None) -> Circuit:
236
+ def serialize_circuit(self, circuit: QuantumCircuit, qubit_index_to_name: dict[int, str] | None = None) -> Circuit:
240
237
  """Serialize a quantum circuit into the IQM data transfer format.
241
238
 
242
239
  Serializing is not strictly bound to the native gateset, i.e. some gates that are not explicitly mentioned in
@@ -254,8 +251,8 @@ class IQMBackend(IQMBackendBase):
254
251
 
255
252
  Args:
256
253
  circuit: quantum circuit to serialize
257
- qubit_mapping: Mapping from qubit indices in the circuit to qubit names on the device. If not provided,
258
- :attr:`.IQMBackendBase.index_to_qubit_name` will be used.
254
+ qubit_index_to_name: Mapping from qubit indices in the circuit to qubit names on the device.
255
+ If ``None``, :attr:`.IQMBackendBase.index_to_qubit_name` will be used.
259
256
 
260
257
  Returns:
261
258
  data transfer object representing the circuit
@@ -264,9 +261,9 @@ class IQMBackend(IQMBackendBase):
264
261
  ValueError: circuit contains an unsupported instruction or is not transpiled in general
265
262
 
266
263
  """
267
- if qubit_mapping is None:
268
- qubit_mapping = self._idx_to_qb
269
- instructions = tuple(serialize_instructions(circuit, qubit_index_to_name=qubit_mapping))
264
+ if qubit_index_to_name is None:
265
+ qubit_index_to_name = self._idx_to_qb
266
+ instructions = tuple(serialize_instructions(circuit, qubit_index_to_name=qubit_index_to_name))
270
267
 
271
268
  try:
272
269
  metadata = to_json_dict(circuit.metadata)
@@ -291,7 +288,7 @@ facade_names: dict[str, IQMFakeBackend] = {
291
288
  class IQMFacadeBackend(IQMBackend):
292
289
  """Simulates locally the execution of quantum circuits on a remote mock IQM quantum computer.
293
290
 
294
- This backend is meant to be used to run circuits on a mock IQM server that has no real quantum hardware,
291
+ This backend is meant to be used to run circuits on a mock IQM Server that has no real quantum hardware,
295
292
  and if the mock execution is successful, simulate the circuits locally using an error model that
296
293
  is broadly representative of the mocked QPU. Finally it returns the *simulated results*.
297
294
 
@@ -299,13 +296,13 @@ class IQMFacadeBackend(IQMBackend):
299
296
 
300
297
  .. important::
301
298
 
302
- When using a facade backend, the IQM server URL of :class:`IQMProvider` should always point to a mock environment
299
+ When using a facade backend, the IQM Server URL of :class:`IQMProvider` should always point to a mock environment
303
300
  rather than a real quantum computer, as the execution results from the server will be discarded and replaced by
304
301
  a locally simulated result generated by Qiskit Aer. If you use a real quantum computer with a facade backend,
305
302
  you will just waste your credits and/or computation time.
306
303
 
307
304
  Args:
308
- client: Client instance for communicating with an IQM server.
305
+ client: Client instance for communicating with an IQM Server.
309
306
  name: Name of the fake backend (simulator instance) to use. If None, will be determined automatically based
310
307
  on the static quantum architecture of the server.
311
308
  kwargs: Optional arguments to be passed to the parent class.
@@ -341,7 +338,13 @@ class IQMFacadeBackend(IQMBackend):
341
338
  return False
342
339
  return True
343
340
 
344
- def run(self, run_input: QuantumCircuit | list[QuantumCircuit], **options) -> JobV1:
341
+ def run(
342
+ self,
343
+ run_input: QuantumCircuit | list[QuantumCircuit],
344
+ *,
345
+ use_timeslot: bool = False,
346
+ **options,
347
+ ) -> JobV1:
345
348
  circuits = [run_input] if isinstance(run_input, QuantumCircuit) else run_input
346
349
  circuits_validated_cregs: list[bool] = [self._validate_no_empty_cregs(circuit) for circuit in circuits]
347
350
  if not all(circuits_validated_cregs):
@@ -350,7 +353,7 @@ class IQMFacadeBackend(IQMBackend):
350
353
  "see the user guide."
351
354
  )
352
355
 
353
- iqm_backend_job = super().run(run_input, **options)
356
+ iqm_backend_job = super().run(run_input, use_timeslot=use_timeslot, **options)
354
357
  iqm_backend_job.result() # get and discard results
355
358
  if iqm_backend_job.status() == JobStatus.ERROR:
356
359
  raise RuntimeError("Remote execution did not succeed.")
@@ -360,18 +363,28 @@ class IQMFacadeBackend(IQMBackend):
360
363
  class IQMProvider:
361
364
  """Provider for IQM backends.
362
365
 
363
- IQMProvider connects to a quantum computer through an IQM server.
366
+ IQMProvider connects to a quantum computer through an IQM Server.
364
367
  If the server requires user authentication, you can provide it either using environment
365
368
  variables, or as keyword arguments to IQMProvider. The user authentication kwargs are passed
366
369
  through to :class:`~iqm.iqm_client.iqm_client.IQMClient` as is, and are documented there.
367
370
 
368
371
  Args:
369
- url: URL of the IQM server (e.g. "https://cocos.resonance.meetiqm.com/garnet")
372
+ url: URL of the IQM Server (e.g. "https://resonance.meetiqm.com/").
373
+ quantum_computer: ID or alias of the quantum computer to connect to, if the IQM Server
374
+ instance controls more than one (e.g. "garnet"). ``None`` means connect to the
375
+ default one.
370
376
 
371
377
  """
372
378
 
373
- def __init__(self, url: str, **user_auth_args): # contains keyword args token or tokens_file
379
+ def __init__(
380
+ self,
381
+ url: str,
382
+ *,
383
+ quantum_computer: str | None = None,
384
+ **user_auth_args, # contains keyword args token or tokens_file
385
+ ):
374
386
  self.url = url
387
+ self.quantum_computer = quantum_computer
375
388
  self.user_auth_args = user_auth_args
376
389
 
377
390
  def get_backend(
@@ -396,7 +409,7 @@ class IQMProvider:
396
409
  Backend instance for connecting to a quantum computer.
397
410
 
398
411
  """
399
- client = IQMClient(self.url, **self.user_auth_args)
412
+ client = IQMClient(self.url, quantum_computer=self.quantum_computer, **self.user_auth_args)
400
413
 
401
414
  if name and name.startswith("facade_"):
402
415
  return IQMFacadeBackend(client, name=name, calibration_set_id=calibration_set_id, use_metrics=use_metrics)
@@ -19,18 +19,15 @@ from collections.abc import Iterable
19
19
  import logging
20
20
  from typing import TypeAlias
21
21
 
22
- from iqm.iqm_client import (
23
- DynamicQuantumArchitecture,
24
- GateImplementationInfo,
25
- GateInfo,
26
- ObservationFinder,
27
- )
22
+ from iqm.iqm_client import ObservationFinder
28
23
  from iqm.qiskit_iqm.move_gate import MoveGate
29
- from qiskit.circuit import Delay, Gate, Parameter, Reset
24
+ from qiskit.circuit import Delay, Gate, IfElseOp, Parameter, Reset
30
25
  from qiskit.circuit.library import CZGate, IGate, Measure, RGate
31
26
  from qiskit.providers import QubitProperties
32
27
  from qiskit.transpiler import InstructionProperties, Target
33
28
 
29
+ from iqm.station_control.interface.models import DynamicQuantumArchitecture, GateImplementationInfo, GateInfo
30
+
34
31
  Locus: TypeAlias = tuple[str, ...]
35
32
  """Sequence of QPU component names on which a gate acts."""
36
33
  LocusIdx: TypeAlias = tuple[int, ...]
@@ -44,6 +41,7 @@ _QISKIT_IQM_GATE_MAP: dict[str, Gate] = {
44
41
  "cz": CZGate(),
45
42
  "move": MoveGate(),
46
43
  "id": IGate(),
44
+ "if_else": IfElseOp,
47
45
  }
48
46
  """Maps IQM native operation names to corresponding Qiskit gate objects."""
49
47
 
@@ -82,6 +80,7 @@ class IQMTarget(Target):
82
80
  # (2) make all the IQMTarget.__init__ args keyword-only with non-colliding names, and init
83
81
  # the necessary superclass attributes ourselves.
84
82
  # (1) seems to break pickling during concurrent transpilation using Qiskit's ``transpile``, so we use (2).
83
+ # (2) is broken from Qiskit 2.2 onwards because Target.__init__ no longer has **kwargs nor *args.
85
84
  self.qubit_properties = self._create_qubit_properties(architecture.qubits, metrics)
86
85
 
87
86
  # Using iqm_ as a prefix to avoid name clashes with the base class.
@@ -126,8 +125,13 @@ class IQMTarget(Target):
126
125
  if "prx" in op_loci:
127
126
  add_gate("prx")
128
127
 
129
- # HACK reset gate shares cc_prx loci for now, until reset is also in the DQA/metrics
130
128
  if "cc_prx" in op_loci:
129
+ # IfElseOp is a global 'gate' so it's slightly different from the others.
130
+ self.add_instruction(
131
+ instruction=_QISKIT_IQM_GATE_MAP["if_else"],
132
+ name="if_else",
133
+ )
134
+ # HACK reset gate shares cc_prx loci for now, until reset is also in the DQA/metrics
131
135
  self.add_instruction(
132
136
  _QISKIT_IQM_GATE_MAP["reset"],
133
137
  {self.locus_to_idx(locus): None for locus in op_loci["cc_prx"]},
@@ -17,10 +17,14 @@ import math
17
17
  import warnings
18
18
 
19
19
  import numpy as np
20
+ from packaging.version import Version
20
21
  from qiskit import QuantumCircuit
22
+ from qiskit import __version__ as qiskit_version
23
+ from qiskit.circuit.controlflow import IfElseOp
21
24
  from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary
22
- from qiskit.circuit.library import RGate, UnitaryGate
23
- from qiskit.dagcircuit import DAGCircuit
25
+ from qiskit.circuit.library import RGate, RZGate, UnitaryGate
26
+ from qiskit.converters import circuit_to_dag, dag_to_circuit
27
+ from qiskit.dagcircuit import DAGCircuit, DAGOpNode
24
28
  from qiskit.transpiler.basepasses import TransformationPass
25
29
  from qiskit.transpiler.passes import (
26
30
  BasisTranslator,
@@ -58,16 +62,33 @@ class IQMOptimizeSingleQubitGates(TransformationPass):
58
62
 
59
63
  def __init__(self, drop_final_rz: bool = True, ignore_barriers: bool = False):
60
64
  super().__init__()
61
- self._basis = ["r", "cz", "move"]
62
- self._intermediate_basis = ["u", "cz", "move"]
65
+ self._basis = ["r", "cz", "move", "if_else"]
66
+ self._intermediate_basis = ["u", "cz", "move", "if_else"]
63
67
  self._drop_final_rz = drop_final_rz
64
68
  self._ignore_barriers = ignore_barriers
65
69
 
66
- def run(self, dag: DAGCircuit) -> DAGCircuit:
67
- self._validate_ops(dag)
70
+ def run(self, dag: DAGCircuit, decompose_rz_to_r: bool = True) -> DAGCircuit:
71
+ """Runs the single-qubit gate optimization pass.
72
+
73
+ Args:
74
+ dag: The input DAG circuit to optimize.
75
+ decompose_rz_to_r: Whether to decompose RZ gates into R gates, or add the to the DAG as
76
+ RZ gates. This is used in recursive calls to communicate the accumulated RZ angles in ``rz_angles``.
77
+
78
+ Returns:
79
+ The optimized DAG circuit.
80
+
81
+ """
82
+ if decompose_rz_to_r:
83
+ self._validate_ops(dag)
68
84
  # accumulated RZ angles for each qubit, from the beginning of the circuit to the current gate
69
85
  rz_angles: list[float] = [0] * dag.num_qubits()
70
86
 
87
+ # Handle old conditional gates
88
+ if Version(qiskit_version) < Version("2.0"):
89
+ # This needs to be done before the BasisTranslation as that pass does not retain the condition.
90
+ dag = self._handle_c_if_blocks(dag)
91
+
71
92
  if self._ignore_barriers:
72
93
  dag = RemoveBarriers().run(dag)
73
94
  # convert all gates in the circuit to U and CZ gates
@@ -76,21 +97,7 @@ class IQMOptimizeSingleQubitGates(TransformationPass):
76
97
  dag = Optimize1qGatesDecomposition(self._intermediate_basis).run(dag)
77
98
  for node in dag.topological_op_nodes():
78
99
  if node.name == "u":
79
- # convert into PRX + RZ
80
- qubit_index = dag.find_bit(node.qargs[0]).index
81
- if isinstance(node.op.params[0], float) and math.isclose(node.op.params[0], 0, abs_tol=TOLERANCE):
82
- dag.remove_op_node(node)
83
- else:
84
- dag.substitute_node(
85
- node,
86
- RGate(
87
- node.op.params[0],
88
- np.pi / 2 - node.op.params[2] - rz_angles[qubit_index],
89
- ),
90
- )
91
- phase = node.op.params[1] + node.op.params[2]
92
- dag.global_phase += phase / 2
93
- rz_angles[qubit_index] += phase
100
+ dag, rz_angles = self._handle_u_gates(dag, node, rz_angles)
94
101
  elif node.name in {"measure", "reset"}:
95
102
  # measure and reset destroy phase information. The local phases before and after such
96
103
  # an operation are in principle independent, and the local computational frame phases
@@ -115,21 +122,207 @@ class IQMOptimizeSingleQubitGates(TransformationPass):
115
122
  rz_angles[res], rz_angles[qb] = rz_angles[qb], rz_angles[res]
116
123
  elif node.name in {"cz", "delay"}:
117
124
  pass # commutes with RZ gates
125
+ elif node.name == "if_else":
126
+ dag, rz_angles = self._handle_if_else_block(dag, node, rz_angles)
118
127
  else:
119
128
  raise ValueError(
120
129
  f"Unexpected operation '{node.name}' in circuit given to IQMOptimizeSingleQubitGates pass"
121
130
  )
122
131
 
123
- if not self._drop_final_rz:
132
+ if not decompose_rz_to_r:
124
133
  for qubit_index, rz_angle in enumerate(rz_angles):
125
- if rz_angle != 0:
126
- qubit = dag.qubits[qubit_index]
127
- dag.apply_operation_back(RGate(-np.pi, 0), qargs=(qubit,))
128
- dag.apply_operation_back(RGate(np.pi, rz_angle / 2), qargs=(qubit,))
134
+ dag.apply_operation_back(RZGate(rz_angle), qargs=(dag.qubits[qubit_index],))
135
+ elif not self._drop_final_rz:
136
+ dag, rz_angles = self._apply_final_r_gates(dag, rz_angles)
129
137
 
130
138
  return dag
131
139
 
140
+ def _apply_final_r_gates(self, dag: DAGCircuit, rz_angles: list[float]) -> tuple[DAGCircuit, list[float]]:
141
+ """Helper function that adds the final PRX/R gates to the circuit according to the accumulated angles.
142
+
143
+ Returns the updated dag and a list of zero angles since the final RZ rotations are already applied.
144
+
145
+ Args:
146
+ dag: The input DAG circuit we are optimizing.
147
+ rz_angles: The accumulated RZ angles for each qubit.
148
+
149
+ Returns:
150
+ The updated DAG circuit and a list of zero angles.
151
+
152
+ """
153
+ for qubit_index, rz_angle in enumerate(rz_angles):
154
+ if not math.isclose(rz_angle, 0, abs_tol=TOLERANCE):
155
+ qubit = dag.qubits[qubit_index]
156
+ dag.apply_operation_back(RGate(-np.pi, 0), qargs=(qubit,))
157
+ dag.apply_operation_back(RGate(np.pi, rz_angle / 2), qargs=(qubit,))
158
+ # Return resetted angles
159
+ return dag, [0.0] * dag.num_qubits()
160
+
161
+ def _handle_u_gates(
162
+ self, dag: DAGCircuit, node: DAGOpNode, rz_angles: list[float]
163
+ ) -> tuple[DAGCircuit, list[float]]:
164
+ """Helper function that converts U gates to PRXs and RZ gates,
165
+ so that the RZ gates can be commuted to the end of the circuit.
166
+
167
+ Args:
168
+ dag: The input DAG circuit we are optimizing.
169
+ node: The DAG node containing the U gate to convert.
170
+ rz_angles: The accumulated RZ angles for each qubit.
171
+
172
+ Returns:
173
+ The updated DAG circuit and the updated list of accumulated RZ angles.
174
+
175
+ """
176
+ qubit_index = dag.find_bit(node.qargs[0]).index
177
+ if isinstance(node.op.params[0], float) and math.isclose(node.op.params[0], 0, abs_tol=TOLERANCE):
178
+ dag.remove_op_node(node)
179
+ else:
180
+ dag.substitute_node(
181
+ node,
182
+ RGate(
183
+ node.op.params[0],
184
+ np.pi / 2 - node.op.params[2] - rz_angles[qubit_index],
185
+ ),
186
+ )
187
+ phase = node.op.params[1] + node.op.params[2]
188
+ dag.global_phase += phase / 2
189
+ rz_angles[qubit_index] += phase
190
+ return dag, rz_angles
191
+
192
+ def _handle_if_else_block(
193
+ self, dag: DAGCircuit, node: DAGOpNode, rz_angles: list[float]
194
+ ) -> tuple[DAGCircuit, list[float]]:
195
+ """Call the optimization recursively on both branches of the if_else node.
196
+
197
+ The accumulated RZ angles are added to both branches before optimizing them.
198
+ The accumulated RZ angles after the optimization are taken from the else branch
199
+ and the adjoint is applied to the if branch to correct for the overrotation.
200
+
201
+ Args:
202
+ dag: The input DAG circuit we are optimizing.
203
+ node: The DAG node containing the if_else block to optimize.
204
+ rz_angles: The accumulated RZ angles for each qubit.
205
+
206
+ Returns:
207
+ The updated DAG circuit and the updated list of accumulated RZ angles.
208
+
209
+ """
210
+ # Add the Rz angles to each circuit block of the if_else node
211
+ # and run this pass recursively
212
+ sub_dags = []
213
+ for circuit_block in node.op.params:
214
+ new_circuit = QuantumCircuit(list(node.qargs + node.cargs))
215
+ # Prepend Rz angle to circuit block
216
+ for qubit in node.qargs:
217
+ new_circuit.append(RGate(-np.pi, 0), [qubit])
218
+ new_circuit.append(RGate(np.pi, rz_angles[dag.find_bit(qubit).index] / 2), [qubit])
219
+ if circuit_block is not None:
220
+ new_circuit.compose(circuit_block, node.qargs, node.cargs, inplace=True)
221
+ # Run optimization pass on the block
222
+ block_dag = circuit_to_dag(new_circuit)
223
+ block_dag = self.run(block_dag, decompose_rz_to_r=False)
224
+ sub_dags.append(block_dag)
225
+ # Pick up the final rotation
226
+ for qubit in node.qargs:
227
+ # Find the last node on the qubit
228
+ final_rzs = [list(block_dag.nodes_on_wire(qubit, only_ops=True))[-1] for block_dag in sub_dags]
229
+ # Assertions because this cannot go wrong by user error
230
+ assert len(final_rzs) == 2, "IfElseOp should have exactly two circuit blocks"
231
+ assert final_rzs[0].name == "rz" and final_rzs[1].name == "rz", (
232
+ "The last operation on each qubit in an IfElseOp should be an RZ gate, "
233
+ + f"found {final_rzs[0].name} and {final_rzs[1].name} instead"
234
+ )
235
+ # Extract the angles
236
+ rz1, rz2 = final_rzs[0].op.params[0], final_rzs[1].op.params[0]
237
+ # We take the else_block rotation as the one to continue pushing through the circuit
238
+ # because we don't support else_blocks in the circuit at the moment.
239
+ # Update the rz_angle on this qubit with the one found
240
+ rz_angles[dag.find_bit(qubit).index] = rz2
241
+ # Remove the final rz from the dag in both circuit blocks
242
+ for block_dag, final_node in zip(sub_dags, final_rzs):
243
+ block_dag.remove_op_node(final_node)
244
+ # Fix the overrotation of the if_block when the final Rz does not match
245
+ if not math.isclose(rz1, rz2):
246
+ rz_angle = rz1 - rz2
247
+ sub_dags[0].apply_operation_back(RGate(-np.pi, 0), qargs=(qubit,))
248
+ sub_dags[0].apply_operation_back(RGate(np.pi, rz_angle / 2), qargs=(qubit,))
249
+ # Replace the params in the if_else node with the optimized circuits
250
+ new_params = []
251
+ for idx, sub_dag in enumerate(sub_dags):
252
+ # Optimize the PRXs on the block_dag, but now keep the final Rzs
253
+ block_dag = IQMOptimizeSingleQubitGates(drop_final_rz=False, ignore_barriers=self._ignore_barriers).run(
254
+ sub_dag
255
+ )
256
+ # Ensure the qubits act on the same qubits as before
257
+ if node.op.params[idx] is not None and block_dag.qubits != node.op.params[idx].qubits:
258
+ # Sometimes the circuit_block.qubits != node.qargs,
259
+ # so we need to make sure that they act on the same qubits as before
260
+ new_circuit = QuantumCircuit(list(node.op.params[idx].qubits + node.op.params[idx].clbits))
261
+ new_circuit.compose(
262
+ dag_to_circuit(block_dag),
263
+ node.op.params[idx].qubits,
264
+ node.op.params[idx].clbits,
265
+ inplace=True,
266
+ )
267
+ else:
268
+ new_circuit = dag_to_circuit(block_dag)
269
+ new_params.append(new_circuit)
270
+ dag.substitute_node(
271
+ node,
272
+ IfElseOp(
273
+ node.op.condition,
274
+ new_params[0],
275
+ false_body=new_params[1] if new_params[1].size() > 0 else None,
276
+ label=node.op.label,
277
+ ),
278
+ )
279
+ return dag, rz_angles
280
+
281
+ def _handle_c_if_blocks(self, dag: DAGCircuit) -> DAGCircuit:
282
+ """Helper function that replaces all classically controlled RGates with an if_else operator.
283
+
284
+ This is needed because the BasisTranslator pass does not retain the condition on the nodes.
285
+ This is only needed for Qiskit versions < 2.0.0.
286
+
287
+ Args:
288
+ dag: The input DAG circuit we are optimizing.
289
+
290
+ Returns:
291
+ The updated DAG circuit with if_else blocks instead of R gates with a condition.
292
+
293
+ """
294
+ for node in dag.topological_op_nodes():
295
+ if hasattr(node, "condition") and node.condition and node.name != "if_else":
296
+ # Manually parse the node to a circuit because helper functions don't exist
297
+ # NOTE if_block needs to have the same size as node or else it cannot be replaced later.
298
+ if_block = QuantumCircuit(list(node.qargs))
299
+ # NOTE Need to reconstruct the node.op manually because rust panics when using node.op directly
300
+ if node.op.name != "r":
301
+ raise ValueError(
302
+ f"Unexpected operation '{node.name}' in circuit given to IQMOptimizeSingleQubitGates pass"
303
+ )
304
+ if_block.append(RGate(node.op.params[0], node.op.params[1], label=node.op.label), node.qargs)
305
+ new_op = IfElseOp(
306
+ node.condition,
307
+ if_block,
308
+ )
309
+ dag.substitute_node(
310
+ node,
311
+ new_op,
312
+ )
313
+ return dag
314
+
132
315
  def _validate_ops(self, dag: DAGCircuit): # noqa: ANN202
316
+ """Helper function that validates that the operations in the circuit are compatible
317
+ with the IQMOptimizeSingleQubitGates pass.
318
+
319
+ Args:
320
+ dag: The input DAG circuit to validate before optimization.
321
+
322
+ Raises:
323
+ ValueError: If an invalid operation is found in the circuit.
324
+
325
+ """
133
326
  valid_ops = self._basis + ["measure", "reset", "delay", "barrier"]
134
327
  for node in dag.op_nodes():
135
328
  if node.name not in valid_ops: