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.
- iqm/cirq_iqm/devices/iqm_device_metadata.py +2 -1
- iqm/cirq_iqm/examples/demo_common.py +1 -1
- iqm/cirq_iqm/examples/demo_iqm_execution.py +3 -3
- iqm/cirq_iqm/iqm_sampler.py +47 -29
- iqm/cirq_iqm/serialize.py +1 -1
- iqm/cirq_iqm/transpiler.py +3 -1
- iqm/iqm_client/__init__.py +0 -2
- iqm/iqm_client/errors.py +6 -17
- iqm/iqm_client/iqm_client.py +199 -602
- iqm/iqm_client/models.py +20 -611
- iqm/iqm_client/transpile.py +11 -8
- iqm/iqm_client/validation.py +18 -9
- iqm/iqm_server_client/__init__.py +14 -0
- iqm/iqm_server_client/errors.py +6 -0
- iqm/iqm_server_client/iqm_server_client.py +755 -0
- iqm/iqm_server_client/models.py +179 -0
- iqm/iqm_server_client/py.typed +0 -0
- iqm/qiskit_iqm/__init__.py +8 -0
- iqm/qiskit_iqm/examples/bell_measure.py +2 -2
- iqm/qiskit_iqm/examples/transpile_example.py +9 -4
- iqm/qiskit_iqm/fake_backends/fake_adonis.py +2 -1
- iqm/qiskit_iqm/fake_backends/fake_aphrodite.py +2 -1
- iqm/qiskit_iqm/fake_backends/fake_apollo.py +2 -1
- iqm/qiskit_iqm/fake_backends/fake_deneb.py +2 -1
- iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py +8 -7
- iqm/qiskit_iqm/iqm_backend.py +3 -4
- iqm/qiskit_iqm/iqm_circuit_validation.py +8 -7
- iqm/qiskit_iqm/iqm_job.py +106 -88
- iqm/qiskit_iqm/iqm_move_layout.py +2 -1
- iqm/qiskit_iqm/iqm_naive_move_pass.py +114 -55
- iqm/qiskit_iqm/iqm_provider.py +49 -36
- iqm/qiskit_iqm/iqm_target.py +4 -6
- iqm/qiskit_iqm/qiskit_to_iqm.py +62 -25
- {iqm_client-32.1.1.dist-info → iqm_client-33.0.0.dist-info}/METADATA +4 -14
- iqm_client-33.0.0.dist-info/RECORD +63 -0
- iqm/iqm_client/api.py +0 -90
- iqm/iqm_client/authentication.py +0 -206
- iqm_client-32.1.1.dist-info/RECORD +0 -60
- {iqm_client-32.1.1.dist-info → iqm_client-33.0.0.dist-info}/AUTHORS.rst +0 -0
- {iqm_client-32.1.1.dist-info → iqm_client-33.0.0.dist-info}/LICENSE.txt +0 -0
- {iqm_client-32.1.1.dist-info → iqm_client-33.0.0.dist-info}/WHEEL +0 -0
- {iqm_client-32.1.1.dist-info → iqm_client-33.0.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
25
|
+
CircuitBatch,
|
|
26
|
+
CircuitJob,
|
|
27
27
|
CircuitMeasurementResults,
|
|
28
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
57
|
-
super().__init__(backend, job_id=job_id
|
|
58
|
-
self.
|
|
59
|
-
self.
|
|
60
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
91
|
+
qiskit_measurement_results,
|
|
92
|
+
Counts(Counter(qiskit_measurement_results)),
|
|
87
93
|
)
|
|
88
|
-
for i, (
|
|
94
|
+
for i, (qiskit_measurement_results, circuit) in enumerate(zip(qiskit_measurement_results_batch, circuits))
|
|
89
95
|
]
|
|
90
96
|
|
|
91
97
|
@staticmethod
|
|
92
|
-
def
|
|
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.
|
|
165
|
+
self._iqm_job.cancel()
|
|
160
166
|
return True
|
|
161
|
-
except
|
|
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
|
|
172
|
+
"""Retrieve job result within defined timeout.
|
|
167
173
|
|
|
168
174
|
Args:
|
|
169
|
-
timeout: Time limit for
|
|
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
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
"
|
|
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":
|
|
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":
|
|
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.
|
|
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
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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
|
|
256
|
+
if status == IQMJobStatus.FAILED:
|
|
242
257
|
return JobStatus.ERROR
|
|
243
|
-
if
|
|
258
|
+
if status == IQMJobStatus.CANCELLED:
|
|
244
259
|
return JobStatus.CANCELLED
|
|
245
|
-
|
|
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
|
-
|
|
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
|
|
38
|
-
This pass is a wrapper that converts the circuit into the
|
|
39
|
-
runs the
|
|
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
|
|
65
|
+
dag: DAG to insert MOVE gates into.
|
|
64
66
|
|
|
65
67
|
Returns:
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
86
|
-
|
|
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:
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
126
|
-
new_dag = circuit_to_dag(
|
|
211
|
+
return circuit_to_dag(
|
|
127
212
|
routed_circuit,
|
|
128
|
-
qubit_order=
|
|
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,
|