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.
- 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 +115 -56
- iqm/qiskit_iqm/iqm_provider.py +49 -36
- iqm/qiskit_iqm/iqm_target.py +12 -8
- iqm/qiskit_iqm/iqm_transpilation.py +219 -26
- iqm/qiskit_iqm/qiskit_to_iqm.py +150 -41
- iqm/qiskit_iqm/transpiler_plugins.py +11 -8
- {iqm_client-32.0.0.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.0.0.dist-info/RECORD +0 -60
- {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/AUTHORS.rst +0 -0
- {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/LICENSE.txt +0 -0
- {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/WHEEL +0 -0
- {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/entry_points.txt +0 -0
- {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/top_level.txt +0 -0
iqm/qiskit_iqm/iqm_provider.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
127
|
-
job = IQMJob(self,
|
|
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
|
-
|
|
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
|
-
|
|
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 = [
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
268
|
-
|
|
269
|
-
instructions = tuple(serialize_instructions(circuit, qubit_index_to_name=
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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__(
|
|
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)
|
iqm/qiskit_iqm/iqm_target.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
132
|
+
if not decompose_rz_to_r:
|
|
124
133
|
for qubit_index, rz_angle in enumerate(rz_angles):
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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:
|