iqm-client 32.1.1__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 (43) 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 +114 -55
  31. iqm/qiskit_iqm/iqm_provider.py +49 -36
  32. iqm/qiskit_iqm/iqm_target.py +4 -6
  33. iqm/qiskit_iqm/qiskit_to_iqm.py +62 -25
  34. {iqm_client-32.1.1.dist-info → iqm_client-33.0.0.dist-info}/METADATA +4 -14
  35. iqm_client-33.0.0.dist-info/RECORD +63 -0
  36. iqm/iqm_client/api.py +0 -90
  37. iqm/iqm_client/authentication.py +0 -206
  38. iqm_client-32.1.1.dist-info/RECORD +0 -60
  39. {iqm_client-32.1.1.dist-info → iqm_client-33.0.0.dist-info}/AUTHORS.rst +0 -0
  40. {iqm_client-32.1.1.dist-info → iqm_client-33.0.0.dist-info}/LICENSE.txt +0 -0
  41. {iqm_client-32.1.1.dist-info → iqm_client-33.0.0.dist-info}/WHEEL +0 -0
  42. {iqm_client-32.1.1.dist-info → iqm_client-33.0.0.dist-info}/entry_points.txt +0 -0
  43. {iqm_client-32.1.1.dist-info → iqm_client-33.0.0.dist-info}/top_level.txt +0 -0
iqm/qiskit_iqm/iqm_job.py CHANGED
@@ -17,79 +17,85 @@ from __future__ import annotations
17
17
 
18
18
  from collections import Counter
19
19
  from datetime import date
20
- from typing import TYPE_CHECKING
21
- import uuid
20
+ from typing import TYPE_CHECKING, Any
22
21
  import warnings
23
22
 
24
23
  from iqm.iqm_client import (
25
24
  DEFAULT_TIMEOUT_SECONDS,
26
- APITimeoutError,
25
+ CircuitBatch,
26
+ CircuitJob,
27
27
  CircuitMeasurementResults,
28
- HeraldingMode,
29
- IQMClient,
30
- JobAbortionError,
31
- RunRequest,
32
- RunResult,
33
- Status,
28
+ CircuitMeasurementResultsBatch,
34
29
  )
30
+ from iqm.iqm_client import (
31
+ JobStatus as IQMJobStatus,
32
+ )
33
+ from iqm.iqm_client.errors import APITimeoutError
35
34
  from iqm.qiskit_iqm.qiskit_to_iqm import MeasurementKey
36
35
  import numpy as np
37
36
  from qiskit.providers import JobStatus, JobV1
38
37
  from qiskit.result import Counts, Result
39
38
 
39
+ from exa.common.errors.station_control_errors import StationControlError
40
40
  from iqm.pulse import Circuit
41
+ from iqm.station_control.interface.models import HeraldingMode
41
42
 
42
43
  if TYPE_CHECKING:
43
44
  from iqm.qiskit_iqm.iqm_provider import IQMBackend
44
45
 
45
46
 
46
47
  class IQMJob(JobV1):
47
- """Implementation of Qiskit's job interface to handle circuit execution on an IQM server.
48
+ """Implementation of Qiskit's job interface to handle circuit execution on an IQM Server.
48
49
 
49
50
  Args:
50
51
  backend: Backend instance initiating this job.
51
- job_id: String representation of the UUID generated by IQM server.
52
+ job: Circuit execution job generated by IQM Server.
52
53
  kwargs: Arguments to be passed to the initializer of the parent class.
53
54
 
54
55
  """
55
56
 
56
- def __init__(self, backend: IQMBackend, job_id: str, **kwargs):
57
- super().__init__(backend, job_id=job_id, **kwargs)
58
- self._result: None | list[tuple[str, list[str]]] = None
59
- self._calibration_set_id: uuid.UUID | None = None
60
- self._request: RunRequest | None = None
61
- self._client: IQMClient = backend.client
57
+ def __init__(self, backend: IQMBackend, job: CircuitJob, **kwargs):
58
+ super().__init__(backend, job_id=str(job.job_id))
59
+ # kwargs go into self.metadata, Qiskit doesn't currently do anything with it
60
+ self._iqm_job = job
61
+ self._iqm_result: None | list[tuple[str, list[str], Counts]] = None
62
62
  self.circuit_metadata: list | None = None # Metadata that was originally associated with circuits by user
63
63
 
64
- def _format_iqm_results(self, iqm_result: RunResult) -> list[tuple[str, list[str]]]:
64
+ @staticmethod
65
+ def _iqm_format_results(
66
+ iqm_results_batch: CircuitMeasurementResultsBatch,
67
+ circuits: CircuitBatch,
68
+ requested_shots: int,
69
+ expect_exact_shots: bool,
70
+ ) -> list[tuple[str, list[str], Counts]]:
65
71
  """Convert the measurement results for a batch of circuits into the Qiskit format.
66
72
 
67
73
  Args:
68
- iqm_result: measurement results for the circuit batch
74
+ iqm_results_batch: measurement results for the circuit batch
75
+ circuits: circuits that were executed
76
+ requested_shots: number of shots requested
77
+ expect_exact_shots: iff True, we must get exactly as many shots as requested
69
78
  Returns:
70
- A list of (circuit_name, measurements) tuples, one tuple for each circuit in the batch.
71
- The measurements are a list of bitstrings, one per shot, representing the state of the classical
79
+ ``(circuit_name, measurements, counts)`` tuples, one tuple for each circuit in the batch.
80
+ ``measurements`` are a list of bitstrings, one per shot, representing the state of the classical
72
81
  registers after the shot.
73
82
 
74
83
  """
75
- if iqm_result.measurements is None:
76
- raise ValueError(
77
- f'Cannot format IQM result without measurements. Job status is "{iqm_result.status.value.upper()}"'
78
- )
79
- requested_shots = self.metadata.get("shots", iqm_result.metadata.shots)
80
- # If no heralding, for all circuits we expect the same number of shots which is the shots requested by user.
81
- expect_exact_shots = iqm_result.metadata.heralding_mode == HeraldingMode.NONE
82
-
84
+ qiskit_measurement_results_batch = [
85
+ IQMJob._iqm_format_measurement_results(measurement_results, requested_shots, expect_exact_shots)
86
+ for measurement_results in iqm_results_batch
87
+ ]
83
88
  return [
84
89
  (
85
90
  circuit.name if isinstance(circuit, Circuit) else f"circuit_{i}",
86
- self._format_measurement_results(measurements, requested_shots, expect_exact_shots),
91
+ qiskit_measurement_results,
92
+ Counts(Counter(qiskit_measurement_results)),
87
93
  )
88
- for i, (measurements, circuit) in enumerate(zip(iqm_result.measurements, iqm_result.metadata.circuits))
94
+ for i, (qiskit_measurement_results, circuit) in enumerate(zip(qiskit_measurement_results_batch, circuits))
89
95
  ]
90
96
 
91
97
  @staticmethod
92
- def _format_measurement_results(
98
+ def _iqm_format_measurement_results(
93
99
  measurement_results: CircuitMeasurementResults, requested_shots: int, expect_exact_shots: bool = True
94
100
  ) -> list[str]:
95
101
  """Convert the measurement results from a circuit into the Qiskit format.
@@ -156,93 +162,104 @@ class IQMJob(JobV1):
156
162
 
157
163
  """
158
164
  try:
159
- self._client.abort_job(uuid.UUID(self._job_id))
165
+ self._iqm_job.cancel()
160
166
  return True
161
- except JobAbortionError as e:
167
+ except StationControlError as e:
162
168
  warnings.warn(f"Failed to cancel job: {e}")
163
169
  return False
164
170
 
165
171
  def result(self, *, timeout: float = DEFAULT_TIMEOUT_SECONDS, cancel_after_timeout: bool = False) -> Result:
166
- """Retrieve results within defined timeout.
172
+ """Retrieve job result within defined timeout.
167
173
 
168
174
  Args:
169
- timeout: Time limit for client to get result, in seconds.
175
+ timeout: Time limit for the job to finish, in seconds.
170
176
  cancel_after_timeout: Whether client will try to cancel the job if timeout exceeded.
171
177
 
172
178
  Returns:
173
- Result if retrieved successfully.
179
+ Result if job finished within ``timeout``.
174
180
 
175
181
  Raises:
176
182
  APITimeoutError: Waiting for results exceeded timeout.
177
- JobAbortionError: Job failed to abort after timeout exceeded and cancellation requested.
178
183
 
179
184
  """
180
- if not self._result:
181
- # Client will raise an error if it was unable to get the results within the timeout
182
- try:
183
- results = self._client.wait_for_results(uuid.UUID(self._job_id), timeout)
184
- except APITimeoutError as err:
185
- # Cancel the job if client was unable to get the results within the timeout
185
+ job = self._iqm_job
186
+ # TODO we might want to cache these
187
+ circuits, job_parameters = job.payload()
188
+ if job.status not in IQMJobStatus.terminal_statuses():
189
+ # wait for job to finish and cache the result
190
+ status = job.wait_for_completion(timeout_secs=timeout)
191
+ if status not in IQMJobStatus.terminal_statuses():
192
+ msg = f"The job {job.job_id} didn't finish in {timeout} seconds."
186
193
  if cancel_after_timeout:
187
- try:
188
- self._client.abort_job(uuid.UUID(self._job_id))
189
- except JobAbortionError as e:
190
- raise JobAbortionError("Failed to cancel job.") from e
191
- raise APITimeoutError("Job cancelled successfully.") from err
192
- raise
193
- self._calibration_set_id = results.metadata.calibration_set_id
194
- self._request = results.metadata.request
195
- if results.metadata.timestamps is not None:
196
- self.metadata["timestamps"] = results.metadata.timestamps.copy()
197
- self._result = self._format_iqm_results(results)
198
- # IQMBackend.run() populates IQMJob.circuit_metadata, so it may be None if this IQMJob
199
- # was created manually from a job_id. In that case retrieve circuit metadata from
200
- # RunResult.metadata.request.circuits[n].metadata
201
- if self.circuit_metadata is None and results.metadata.request is not None:
202
- self.circuit_metadata = [
203
- c.metadata if isinstance(c, Circuit) else {} for c in results.metadata.circuits
204
- ]
205
-
206
- result_dict = {
207
- "backend_name": None,
208
- "backend_version": None,
209
- "qobj_id": None,
210
- "job_id": self._job_id,
211
- "success": True,
212
- "results": [
194
+ # Cancel the job if client was unable to get the results within the timeout
195
+ job.cancel()
196
+ msg += " Cancelled."
197
+ raise APITimeoutError(msg)
198
+
199
+ # retrieve the results if job completed successfully
200
+ if status == IQMJobStatus.COMPLETED and (result_batch := job.result()) is not None:
201
+ # now job has up-to-date information
202
+ # IQMBackend.run() populates IQMJob.circuit_metadata, so it may be None if this IQMJob
203
+ # was created manually from a job_id. In that case retrieve circuit metadata from
204
+ # the job payload which we just retrieved.
205
+ if self.circuit_metadata is None:
206
+ self.circuit_metadata = [c.metadata if isinstance(c, Circuit) else {} for c in circuits]
207
+
208
+ # If no heralding, for all circuits we expect the same number of shots which is the
209
+ # shots requested by user.
210
+ self._iqm_result = self._iqm_format_results(
211
+ result_batch,
212
+ circuits,
213
+ job_parameters.shots,
214
+ job_parameters.heralding_mode == HeraldingMode.NONE,
215
+ )
216
+
217
+ # job has reached a terminal status, which means we can return a Result
218
+ result_dict: dict[str, Any] = {
219
+ "backend_name": self.backend().name,
220
+ "backend_version": "",
221
+ "qobj_id": "",
222
+ "job_id": str(job.job_id),
223
+ "success": False,
224
+ "date": date.today().isoformat(),
225
+ "results": [],
226
+ # the ones below go into result._metadata
227
+ "circuits": circuits,
228
+ "parameters": job_parameters,
229
+ "timeline": job.data.timeline.copy(),
230
+ }
231
+ if self._iqm_result:
232
+ result_dict["success"] = True # job is successful iff it is IQMJobStatus.COMPLETED (and we got result data)
233
+ result_dict["results"] = [
213
234
  {
214
235
  "shots": len(measurement_results),
215
236
  "success": True,
216
237
  "data": {
217
238
  "memory": measurement_results,
218
- "counts": Counts(Counter(measurement_results)),
239
+ "counts": counts,
219
240
  "metadata": self.circuit_metadata[i] if self.circuit_metadata is not None else {},
220
241
  },
221
242
  "header": {"name": name},
222
- "calibration_set_id": self._calibration_set_id,
243
+ "calibration_set_id": job.data.compilation.calibration_set_id if job.data.compilation else None,
223
244
  }
224
- for i, (name, measurement_results) in enumerate(self._result)
225
- ],
226
- "date": date.today().isoformat(),
227
- "request": self._request,
228
- "timestamps": self.metadata.get("timestamps"),
229
- }
245
+ for i, (name, measurement_results, counts) in enumerate(self._iqm_result)
246
+ ]
230
247
  return Result.from_dict(result_dict)
231
248
 
232
249
  def status(self) -> JobStatus:
233
- if self._result:
234
- return JobStatus.DONE
250
+ job = self._iqm_job
251
+ # terminal statuses need not be updated, they're terminal
252
+ status = job.status if job.status in IQMJobStatus.terminal_statuses() else job.update()
235
253
 
236
- result = self._client.get_run_status(uuid.UUID(self._job_id))
237
- if result.status == Status.PENDING_EXECUTION:
238
- return JobStatus.RUNNING
239
- if result.status == Status.READY:
254
+ if status == IQMJobStatus.COMPLETED:
240
255
  return JobStatus.DONE
241
- if result.status in {Status.FAILED, Status.DELETION_FAILED}:
256
+ if status == IQMJobStatus.FAILED:
242
257
  return JobStatus.ERROR
243
- if result.status in {Status.ABORTED, Status.DELETED}:
258
+ if status == IQMJobStatus.CANCELLED:
244
259
  return JobStatus.CANCELLED
245
- # For everything else, we assume that it's in progress one way or another
260
+ if status == IQMJobStatus.PROCESSING:
261
+ return JobStatus.RUNNING
262
+ # IQMJobStatus.WAITING
246
263
  return JobStatus.QUEUED
247
264
 
248
265
  def queue_position(self, refresh: bool = False) -> int | None:
@@ -264,4 +281,5 @@ class IQMJob(JobV1):
264
281
 
265
282
  def error_message(self) -> str | None:
266
283
  """Returns the error message if job has failed, otherwise returns None."""
267
- return self._client.get_run_status(uuid.UUID(self._job_id)).message
284
+ self._iqm_job.update()
285
+ return self._iqm_job._errors or None
@@ -13,7 +13,6 @@
13
13
  # limitations under the License.
14
14
  """Generate an initial layout for a quantum circuit containing MOVE gates."""
15
15
 
16
- from iqm.iqm_client import DynamicQuantumArchitecture
17
16
  from iqm.qiskit_iqm.iqm_backend import IQMTarget
18
17
  from qiskit import QuantumCircuit
19
18
  from qiskit.circuit import Qubit
@@ -22,6 +21,8 @@ from qiskit.transpiler import PassManager, TranspilerError
22
21
  from qiskit.transpiler.layout import Layout
23
22
  from qiskit.transpiler.passes import TrivialLayout
24
23
 
24
+ from iqm.station_control.interface.models import DynamicQuantumArchitecture
25
+
25
26
 
26
27
  class IQMMoveLayout(TrivialLayout):
27
28
  """Create a layout that is valid on the dynamic quantum architecture of the
@@ -13,9 +13,9 @@
13
13
  # limitations under the License.
14
14
  """Naive transpilation for the IQM Star architecture."""
15
15
 
16
+ from typing import Any
16
17
  import warnings
17
18
 
18
- from iqm.iqm_client import Circuit as IQMClientCircuit
19
19
  from iqm.iqm_client.transpile import ExistingMoveHandlingOptions, transpile_insert_moves
20
20
  import numpy as np
21
21
  from pydantic_core import ValidationError
@@ -26,6 +26,8 @@ from qiskit.dagcircuit import DAGCircuit
26
26
  from qiskit.transpiler.basepasses import TransformationPass
27
27
  from qiskit.transpiler.layout import Layout
28
28
 
29
+ from iqm.pulse import Circuit
30
+
29
31
  from .iqm_backend import IQMBackendBase, IQMTarget
30
32
  from .iqm_move_layout import generate_initial_layout
31
33
  from .qiskit_to_iqm import deserialize_instructions, serialize_instructions
@@ -34,9 +36,9 @@ from .qiskit_to_iqm import deserialize_instructions, serialize_instructions
34
36
  class IQMNaiveResonatorMoving(TransformationPass):
35
37
  """Naive transpilation pass for resonator moving.
36
38
 
37
- The logic of this pass is deferred to `iqm-client.transpile_insert_moves`.
38
- This pass is a wrapper that converts the circuit into the IQMClient Circuit format,
39
- runs the `transpile_insert_moves` function, and then converts the result back to a Qiskit circuit.
39
+ The logic of this pass is deferred to :func:`~iqm.iqm_client.transpile.transpile_insert_moves`.
40
+ This pass is a wrapper that converts the circuit into the IQM circuit format,
41
+ runs the ``transpile_insert_moves`` function, and then converts the result back to a Qiskit circuit.
40
42
 
41
43
  Args:
42
44
  target: Transpilation target.
@@ -60,13 +62,53 @@ class IQMNaiveResonatorMoving(TransformationPass):
60
62
  """Run the pass on a circuit.
61
63
 
62
64
  Args:
63
- dag: DAG to map.
65
+ dag: DAG to insert MOVE gates into.
64
66
 
65
67
  Returns:
66
- Mapped ``dag``.
68
+ The new ``dag`` now including MOVE gates as needed.
67
69
 
68
70
  Raises:
69
- TranspilerError: The layout is not compatible with the DAG, or if the input gate set is incorrect.
71
+ TranspilerError: The layout is not compatible with the DAG, or if something goes wrong during transpilation.
72
+
73
+ """
74
+ if len(dag.op_nodes()) == 0:
75
+ return dag # Empty circuit, no need to transpile.
76
+ symbolic_gates = self._remove_parameterized_gates(dag)
77
+ layout = self._calculate_initial_layout(dag)
78
+ new_dag = self._insert_move_gate(dag, layout)
79
+ self._insert_parameterized_gates(new_dag, symbolic_gates)
80
+ self.property_set["final_layout"] = self._calculate_final_layout(dag, layout)
81
+ return new_dag
82
+
83
+ def _calculate_initial_layout(self, dag: DAGCircuit) -> Layout:
84
+ """Calculate the initial physical to logical qubit layout before the circuit.
85
+
86
+ Args:
87
+ dag: The transpiled DAGCircuit.
88
+
89
+ Returns:
90
+ The initial layout of which logical qubits are in which physical qubits at the start of the circuit.
91
+
92
+ """
93
+ # For some reason, the dag does not always contain the layout, so we need to do a bunch of fixing.
94
+ if self.property_set.get("layout"):
95
+ return self.property_set["layout"]
96
+ # Reconstruct the layout from the dag.
97
+ layout = Layout()
98
+ for qreg in dag.qregs:
99
+ layout.add_register(qreg)
100
+ for i, qubit in enumerate(dag.qubits):
101
+ layout.add(qubit, i)
102
+ return layout
103
+
104
+ def _remove_parameterized_gates(self, dag: DAGCircuit) -> dict[float, tuple[Any, ...]]:
105
+ """Remove parameterized gates from the DAGCircuit.
106
+
107
+ Args:
108
+ dag: The input DAGCircuit, which is changed in place.
109
+
110
+ Returns:
111
+ A mapping from symbolic index to the original gate parameters.
70
112
 
71
113
  """
72
114
  # TODO: Temporary hack to get the symbolic parameters to work: replace symbols with (inf, idx).
@@ -80,81 +122,98 @@ class IQMNaiveResonatorMoving(TransformationPass):
80
122
  symbolic_gates[symbolic_index] = node.op.params
81
123
  dag.substitute_node(node, RGate(np.inf, float(symbolic_index)))
82
124
  symbolic_index += 1
125
+ return symbolic_gates
126
+
127
+ def _insert_parameterized_gates(self, dag: DAGCircuit, symbolic_gates: dict[float, tuple[Any, ...]]) -> None:
128
+ """Reinsert parameterized gates into the DAGCircuit.
129
+
130
+ Args:
131
+ dag: The input DAGCircuit, which is changed in place.
132
+ symbolic_gates: A mapping from symbolic index to the original gate parameters.
83
133
 
134
+ """
135
+ for node in dag.topological_op_nodes():
136
+ # This only works for prx gates because that has two parameters
137
+ # We use one to mark that it is a symbolic gate (np.inf) and the other to store the index.
138
+ if node.name == "r" and not np.isfinite(node.op.params[0]):
139
+ dag.substitute_node(node, RGate(*symbolic_gates[int(np.round(node.op.params[1]))]))
140
+
141
+ def _calculate_final_layout(self, dag: DAGCircuit, layout: Layout) -> Layout:
142
+ """Calculate the final physical to logical qubit layout after the circuit.
143
+ The original final layout from the previous passes uses the old qubit registers that are no longer valid.
144
+
145
+ Args:
146
+ dag: The original DAGCircuit without the inserted MOVE gates..
147
+ layout: The initial physical to logical qubit layout at the start of the circuit.
148
+
149
+ Returns:
150
+ The updated layout of which logical qubits are in which physical qubits at the end of the circuit.
151
+
152
+ """
153
+ if "final_layout" not in self.property_set or self.property_set["final_layout"] is None:
154
+ # If the final layout is not set, return the initial layout.
155
+ return layout
156
+ # This final layout only has a single QuantumRegister where the qubits might be swapped.
157
+ inv_layout = layout.get_physical_bits()
158
+ old_final_layout = self.property_set["final_layout"]
159
+ # Swap the physical bits to the new layout
160
+ to_swap = {
161
+ physical: dag.find_bit(virtual).index for physical, virtual in old_final_layout.get_physical_bits().items()
162
+ }
163
+ # Add identity mappings for qubits that are not in the old final layout.
164
+ to_swap.update({p: p for p in inv_layout if p not in to_swap})
165
+ # Build the new final layout.
166
+ return Layout({physical: inv_layout[to_swap[physical]] for physical in layout.get_physical_bits()})
167
+
168
+ def _insert_move_gate(self, dag: DAGCircuit, layout: Layout) -> DAGCircuit:
169
+ """Insert MOVE gates into the circuit using the IQM transpiler.
170
+
171
+ Args:
172
+ dag: The DAGCircuit to insert MOVE gates into; is not modified in place.
173
+ layout: The physical to logical qubit layout at the start of the circuit.
174
+
175
+ Returns:
176
+ The updated DAGCircuit with MOVE gates inserted.
177
+
178
+ """
179
+ # Convert the DAG to a QuantumCircuit
84
180
  circuit = dag_to_circuit(dag)
85
- if len(circuit) == 0:
86
- return dag # Empty circuit, no need to transpile.
87
- # For some reason, the dag does not contain the layout, so we need to do a bunch of fixing.
88
- if self.property_set.get("layout"):
89
- layout = self.property_set["layout"]
90
- else:
91
- # Reconstruct the layout from the dag.
92
- layout = Layout()
93
- for qreg in dag.qregs:
94
- layout.add_register(qreg)
95
- for i, qubit in enumerate(dag.qubits):
96
- layout.add(qubit, i)
97
-
98
- # Convert the circuit to the IQMClientCircuit format and run the transpiler.
99
- iqm_circuit = IQMClientCircuit(
181
+ # Convert the circuit to the Circuit format
182
+ iqm_circuit = Circuit(
100
183
  name="Transpiling Circuit",
101
184
  instructions=tuple(serialize_instructions(circuit, self.idx_to_component, overwrite_layout=layout)),
102
185
  metadata=None,
103
186
  )
104
187
  try:
188
+ # Use the iqm-client transpiler to insert MOVE gates
105
189
  routed_iqm_circuit = transpile_insert_moves(
106
190
  iqm_circuit,
107
191
  self.architecture,
108
192
  existing_moves=self.existing_moves_handling,
109
193
  )
194
+ # Turn the routed Circuit back into a Qiskit QuantumCircuit
110
195
  routed_circuit = deserialize_instructions(
111
196
  list(routed_iqm_circuit.instructions), self.component_to_idx, layout
112
197
  )
113
- except ValidationError as e: # The Circuit without move gates is empty.
198
+ except ValidationError as e:
114
199
  errors = e.errors()
115
200
  if (
116
201
  len(errors) == 1
117
202
  and errors[0]["msg"] == "Value error, Each circuit should have at least one instruction."
118
- ):
119
- circ_args = [circuit.num_ancillas, circuit.num_clbits]
120
- routed_circuit = QuantumCircuit(*layout.get_registers(), *(arg for arg in circ_args if arg > 0))
203
+ ): # Error because the Circuit without move gates is empty.
204
+ routed_circuit = QuantumCircuit(
205
+ *layout.get_registers(), *(arg for arg in [circuit.num_ancillas, circuit.num_clbits] if arg > 0)
206
+ )
121
207
  else:
122
208
  raise e
123
209
 
124
210
  # Create the new DAG and make sure that the qubits are properly ordered.
125
- ordered_qubits = [layout.get_physical_bits()[i] for i in range(len(layout.get_physical_bits()))]
126
- new_dag = circuit_to_dag(
211
+ return circuit_to_dag(
127
212
  routed_circuit,
128
- qubit_order=ordered_qubits,
213
+ qubit_order=[layout.get_physical_bits()[i] for i in range(len(layout.get_physical_bits()))],
129
214
  clbit_order=routed_circuit.clbits,
130
215
  )
131
216
 
132
- # Reinsert the symbolic parameters.
133
- for node in new_dag.topological_op_nodes():
134
- # This only works for prx gates because that has two parameters
135
- # We use one to mark that it is a symbolic gate (np.inf) and the other to store the index.
136
- if node.name == "r" and not np.isfinite(node.op.params[0]):
137
- new_dag.substitute_node(node, RGate(*symbolic_gates[int(np.round(node.op.params[1]))]))
138
-
139
- # Update the final_layout with the correct bits.
140
- if "final_layout" in self.property_set:
141
- inv_layout = layout.get_physical_bits()
142
- new_final_layout_dict = {
143
- physical: inv_layout[dag.find_bit(virtual).index]
144
- for physical, virtual in self.property_set["final_layout"].get_physical_bits().items()
145
- }
146
- resonator_dict = {
147
- phys: inv_layout[new_dag.find_bit(virt).index]
148
- for phys, virt in inv_layout.items()
149
- if phys not in self.property_set["final_layout"].get_physical_bits()
150
- }
151
- new_final_layout_dict.update(resonator_dict)
152
- self.property_set["final_layout"] = Layout(new_final_layout_dict)
153
- else:
154
- self.property_set["final_layout"] = layout
155
-
156
- return new_dag
157
-
158
217
 
159
218
  def _get_scheduling_method(
160
219
  perform_move_routing: bool,