Skip to content

Commit 662f19b

Browse files
committed
Add fast transpilation method to BaseExperiment
1 parent d7df5f0 commit 662f19b

File tree

10 files changed

+275
-83
lines changed

10 files changed

+275
-83
lines changed

qiskit_experiments/calibration_management/base_calibration_experiment.py

Lines changed: 4 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,13 @@
2121
from qiskit import QuantumCircuit
2222
from qiskit.providers.options import Options
2323
from qiskit.pulse import ScheduleBlock
24-
from qiskit.transpiler import StagedPassManager, PassManager, Layout, CouplingMap
25-
from qiskit.transpiler.passes import (
26-
EnlargeWithAncilla,
27-
FullAncillaAllocation,
28-
ApplyLayout,
29-
SetLayout,
30-
)
3124

3225
from qiskit_experiments.calibration_management.calibrations import Calibrations
3326
from qiskit_experiments.calibration_management.update_library import BaseUpdater
3427
from qiskit_experiments.framework.base_analysis import BaseAnalysis
3528
from qiskit_experiments.framework.base_experiment import BaseExperiment
3629
from qiskit_experiments.framework.experiment_data import ExperimentData
30+
from qiskit_experiments.framework.transpilation import map_qubits, minimal_transpile
3731
from qiskit_experiments.exceptions import CalibrationError
3832

3933
LOG = logging.getLogger(__name__)
@@ -198,20 +192,6 @@ def _default_experiment_options(cls) -> Options:
198192
options.update_options(result_index=-1, group="default")
199193
return options
200194

201-
@classmethod
202-
def _default_transpile_options(cls) -> Options:
203-
"""Return empty default transpile options as optimization_level is not used."""
204-
return Options()
205-
206-
def set_transpile_options(self, **fields):
207-
r"""Add a warning message.
208-
209-
.. note::
210-
If your experiment has overridden `_transpiled_circuits` and needs
211-
transpile options then please also override `set_transpile_options`.
212-
"""
213-
warnings.warn(f"Transpile options are not used in {self.__class__.__name__ }.")
214-
215195
def update_calibrations(self, experiment_data: ExperimentData):
216196
"""Update parameter values in the :class:`.Calibrations` instance.
217197
@@ -295,42 +275,13 @@ def _transpiled_circuits(self) -> List[QuantumCircuit]:
295275
Returns:
296276
A list of transpiled circuits.
297277
"""
298-
transpiled = []
299-
for circ in self.circuits():
300-
circ = self._map_to_physical_qubits(circ)
278+
circuits = [map_qubits(c, self.physical_qubits) for c in self.circuits()]
279+
for circ in circuits:
301280
self._attach_calibrations(circ)
302-
303-
transpiled.append(circ)
281+
transpiled = minimal_transpile(circuits, self.backend, self.transpile_options)
304282

305283
return transpiled
306284

307-
def _map_to_physical_qubits(self, circuit: QuantumCircuit) -> QuantumCircuit:
308-
"""Map program qubits to physical qubits.
309-
310-
Args:
311-
circuit: The quantum circuit to map to device qubits.
312-
313-
Returns:
314-
A quantum circuit that has the same number of qubits as the backend and where
315-
the physical qubits of the experiment have been properly mapped.
316-
"""
317-
initial_layout = Layout.from_intlist(list(self.physical_qubits), *circuit.qregs)
318-
319-
coupling_map = self._backend_data.coupling_map
320-
if coupling_map is not None:
321-
coupling_map = CouplingMap(self._backend_data.coupling_map)
322-
323-
layout = PassManager(
324-
[
325-
SetLayout(initial_layout),
326-
FullAncillaAllocation(coupling_map),
327-
EnlargeWithAncilla(),
328-
ApplyLayout(),
329-
]
330-
)
331-
332-
return StagedPassManager(["layout"], layout=layout).run(circuit)
333-
334285
@abstractmethod
335286
def _attach_calibrations(self, circuit: QuantumCircuit):
336287
"""Attach the calibrations to the quantum circuit.

qiskit_experiments/framework/base_experiment.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,23 @@
1313
Base Experiment class.
1414
"""
1515

16-
from abc import ABC, abstractmethod
1716
import copy
17+
from abc import ABC, abstractmethod
1818
from collections import OrderedDict
1919
from typing import Sequence, Optional, Tuple, List, Dict, Union
2020

21-
from qiskit import transpile, QuantumCircuit
21+
from qiskit import QuantumCircuit
2222
from qiskit.providers import Job, Backend
2323
from qiskit.exceptions import QiskitError
2424
from qiskit.qobj.utils import MeasLevel
2525
from qiskit.providers.options import Options
2626
from qiskit_experiments.framework import BackendData
2727
from qiskit_experiments.framework.store_init_args import StoreInitArgs
28+
from qiskit_experiments.framework.transpilation import (
29+
DEFAULT_TRANSPILE_OPTIONS,
30+
map_qubits,
31+
minimal_transpile,
32+
)
2833
from qiskit_experiments.framework.base_analysis import BaseAnalysis
2934
from qiskit_experiments.framework.experiment_data import ExperimentData
3035
from qiskit_experiments.framework.configs import ExperimentConfig
@@ -373,9 +378,8 @@ def _transpiled_circuits(self) -> List[QuantumCircuit]:
373378
374379
This function can be overridden to define custom transpilation.
375380
"""
376-
transpile_opts = copy.copy(self.transpile_options.__dict__)
377-
transpile_opts["initial_layout"] = list(self.physical_qubits)
378-
transpiled = transpile(self.circuits(), self.backend, **transpile_opts)
381+
circuits = [map_qubits(c, self.physical_qubits) for c in self.circuits()]
382+
transpiled = minimal_transpile(circuits, self.backend, self.transpile_options)
379383

380384
return transpiled
381385

@@ -418,11 +422,36 @@ def set_experiment_options(self, **fields):
418422

419423
@classmethod
420424
def _default_transpile_options(cls) -> Options:
421-
"""Default transpiler options for transpilation of circuits"""
425+
"""Default transpiler options for transpilation of circuits
426+
427+
Transpile Options:
428+
optimization_level (int): Optimization level to pass to
429+
:func:`qiskit.transpile`.
430+
num_processes (int): Number of processes to use during
431+
transpilation on Qiskit >= 1.0.
432+
full_transpile (bool): If ``True``,
433+
``BaseExperiment._transpiled_circuits`` (called by
434+
:meth:`BaseExperiment.run` if not overridden by a subclass)
435+
will call :func:`qiskit.transpile` on the output of
436+
:meth:`BaseExperiment.circuits` before executing the circuits.
437+
If ``False``, ``BaseExperiment._transpiled_circuits`` will
438+
reindex the qubits in the output of
439+
:meth:`BaseExperiment.circuits` using the experiments'
440+
:meth:`BaseExperiment.physical_qubits`. Then it will check if
441+
the circuit operations are all defined in the
442+
:class:`qiskit.transpiler.Target` of the experiment's backend
443+
or in the indiivdual circuit calibrations. If not, it will use
444+
:class:`qiskit.transpiler.passes.BasisTranslator` to map the
445+
circuit instructions to the backend. Additionally,
446+
the :class:`qiskit.transpiler.passes.PulseGates` transpiler
447+
pass will be run if the :class:`qiskit.transpiler.Target`
448+
contains any custom pulse gate calibrations.
449+
450+
"""
422451
# Experiment subclasses can override this method if they need
423452
# to set specific default transpiler options to transpile the
424453
# experiment circuits.
425-
return Options(optimization_level=0)
454+
return copy.copy(DEFAULT_TRANSPILE_OPTIONS)
426455

427456
@property
428457
def transpile_options(self) -> Options:

qiskit_experiments/framework/experiment_data.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -866,11 +866,10 @@ def _add_job_data(
866866
LOG.warning("Job was cancelled before completion [Job ID: %s]", jid)
867867
return jid, False
868868
if status == JobStatus.ERROR:
869-
LOG.error(
870-
"Job data not added for errored job [Job ID: %s]\nError message: %s",
871-
jid,
872-
job.error_message(),
873-
)
869+
msg = f"Job data not added for errored job [Job ID: {jid}]"
870+
if hasattr(job, "error_message"):
871+
msg += f"\nError message: {job.error_message()}"
872+
LOG.error(msg)
874873
return jid, False
875874
LOG.warning("Adding data from job failed [Job ID: %s]", job.job_id())
876875
raise ex
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# This code is part of Qiskit.
2+
#
3+
# (C) Copyright IBM 2021.
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
"""
13+
Functions for preparing circuits for execution
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import importlib.metadata
19+
import logging
20+
from collections.abc import Sequence
21+
22+
from qiskit import QuantumCircuit, QuantumRegister, transpile
23+
from qiskit.exceptions import QiskitError
24+
from qiskit.providers import Backend
25+
from qiskit.providers.options import Options
26+
from qiskit.pulse.calibration_entries import CalibrationPublisher
27+
from qiskit.transpiler import Target
28+
29+
30+
LOGGER = logging.getLogger(__file__)
31+
32+
DEFAULT_TRANSPILE_OPTIONS = Options(optimization_level=0, full_transpile=False)
33+
if importlib.metadata.version("qiskit").partition(".")[0] != "0":
34+
DEFAULT_TRANSPILE_OPTIONS["num_processes"] = 1
35+
36+
37+
def map_qubits(
38+
circuit: QuantumCircuit,
39+
physical_qubits: Sequence[int],
40+
n_qubits: int | None = None,
41+
) -> QuantumCircuit:
42+
"""Generate a new version of a circuit with new qubit indices
43+
44+
This function iterates through the instructions of ``circuit`` and copies
45+
them into a new circuit with qubit indices replaced according to the
46+
entries in ``physical_qubits``. So qubit 0's instructions are applied to
47+
``physical_qubits[0]`` and qubit 1's to ``physical_qubits[1]``, etc.
48+
49+
This function behaves similarly to passing ``initial_layout`` to
50+
:func:`qiskit.transpile` but does not use a Qiskit
51+
:class:`~qiskit.transpiler.PassManager` and does not fill the circuit with
52+
ancillas.
53+
54+
Args:
55+
circuit: The :class:`~qiskit.QuantumCircuit` to re-index.
56+
physical_qubits: The list of new indices for ``circuit``'s qubit indices.
57+
n_qubits: Optional qubit size to use for the output circuit. If
58+
``None``, then the maximum of ``physical_qubits`` will be used.
59+
60+
Returns:
61+
The quantum circuit with new qubit indices
62+
"""
63+
if len(physical_qubits) != circuit.num_qubits:
64+
raise QiskitError(
65+
f"Circuit to map has {circuit.num_qubits} qubits, but "
66+
f"{len(physical_qubits)} physical qubits specified for mapping."
67+
)
68+
69+
# if all(p == r for p, r in zip(physical_qubits, range(circuit.num_qubits))):
70+
# # No mapping necessary
71+
# return circuit
72+
73+
circ_size = n_qubits if n_qubits is not None else (max(physical_qubits) + 1)
74+
p_qregs = QuantumRegister(circ_size)
75+
p_circ = QuantumCircuit(
76+
p_qregs,
77+
*circuit.cregs,
78+
name=circuit.name,
79+
metadata=circuit.metadata,
80+
global_phase=circuit.global_phase,
81+
)
82+
p_circ.compose(
83+
circuit,
84+
qubits=physical_qubits,
85+
inplace=True,
86+
copy=False,
87+
)
88+
return p_circ
89+
90+
91+
def _has_calibration(target: Target, name: str, qubits: tuple[int, ...]) -> bool:
92+
"""Wrapper to work around bug in Target.has_calibration"""
93+
try:
94+
has_cal = target.has_calibration(name, qubits)
95+
except AttributeError:
96+
has_cal = False
97+
98+
return has_cal
99+
100+
101+
def check_transpilation_needed(
102+
circuits: Sequence[QuantumCircuit],
103+
backend: Backend,
104+
) -> bool:
105+
"""Test if circuits are already compatible with backend
106+
107+
This function checks if circuits are able to be executed on ``backend``
108+
without transpilation. It loops through the circuits to check if any gate
109+
instructions are not included in the backend's
110+
:class:`~qiskit.transpiler.Target`. The :class:`~qiskit.transpiler.Target`
111+
is also checked for custom pulse gate calibrations for circuit's
112+
instructions. If all gates are included in the target and there are no
113+
custom calibrations, the function returns ``False`` indicating that
114+
transpilation is not needed.
115+
116+
This function returns ``True`` if the version of ``backend`` is less than
117+
2.
118+
119+
The motivation for this function is that when no transpilation is necessary
120+
it is faster to check the circuits in this way than to run
121+
:func:`~qiskit.transpile` and have it do nothing.
122+
123+
Args:
124+
circuits: The circuits to prepare for the backend.
125+
backend: The backend for which the circuits should be prepared.
126+
127+
Returns:
128+
``True`` if transpilation is needed. Otherwise, ``False``.
129+
"""
130+
transpilation_needed = False
131+
132+
if getattr(backend, "version", 0) <= 1:
133+
# Fall back to transpilation for BackendV1
134+
return True
135+
136+
target = backend.target
137+
138+
for circ in circuits:
139+
for inst in circ.data:
140+
if inst.operation.name == "barrier" or circ.has_calibration_for(inst):
141+
continue
142+
qubits = tuple(circ.find_bit(q).index for q in inst.qubits)
143+
if not target.instruction_supported(inst.operation.name, qubits):
144+
transpilation_needed = True
145+
break
146+
if _has_calibration(target, inst.operation.name, qubits):
147+
cal = target.get_calibration(inst.operation.name, qubits, *inst.operation.params)
148+
if (
149+
cal.metadata.get("publisher", CalibrationPublisher.QISKIT)
150+
!= CalibrationPublisher.BACKEND_PROVIDER
151+
):
152+
transpilation_needed = True
153+
break
154+
if transpilation_needed:
155+
break
156+
157+
return transpilation_needed
158+
159+
160+
def minimal_transpile(
161+
circuits: Sequence[QuantumCircuit],
162+
backend: Backend,
163+
options: Options,
164+
) -> list[QuantumCircuit]:
165+
"""Prepare circuits for execution on a backend
166+
167+
This function is a wrapper around :func:`~qiskit.transpile` to prepare
168+
circuits for execution ``backend`` that tries to do less work in the case
169+
in which the ``circuits`` can already be executed on the backend without
170+
modification.
171+
172+
The instructions in ``circuits`` are checked to see if they can be executed
173+
by the ``backend`` using :func:`check_transpilation_needed`. If the
174+
circuits can not be executed, :func:`~qiskit.transpile` is called on them.
175+
``options`` is a set of options to pass to the :func:`~qiskit.transpile`
176+
(see detailed description of ``options``). The special ``full_transpile``
177+
option can also be set to ``True`` to force calling
178+
:func:`~qiskit.transpile`.
179+
180+
Args:
181+
circuits: The circuits to prepare for the backend.
182+
backend: The backend for which the circuits should be prepared.
183+
options: Options for the transpilation. ``full_transpile`` can be set
184+
to ``True`` to force this function to pass the circuits to
185+
:func:`~qiskit.transpile`. Other options are passed as arguments to
186+
:func:`qiskit.transpile` if it is called.
187+
188+
Returns:
189+
The prepared circuits
190+
"""
191+
options = dict(options.items())
192+
193+
if "full_transpile" not in options:
194+
LOGGER.debug(
195+
"Performing full transpile because base transpile options "
196+
"were overwritten and full_transpile was not specified."
197+
)
198+
full_transpile = True
199+
else:
200+
full_transpile = options.pop("full_transpile", False)
201+
if not full_transpile and set(options) - set(DEFAULT_TRANSPILE_OPTIONS):
202+
# If an experiment specifies transpile options, it needs to go
203+
# through transpile()
204+
full_transpile = True
205+
LOGGER.debug(
206+
"Performing full transpile because non-default transpile options are specified."
207+
)
208+
209+
if not full_transpile:
210+
full_transpile = check_transpilation_needed(circuits, backend)
211+
212+
if full_transpile:
213+
transpiled = transpile(circuits, backend, **options)
214+
else:
215+
transpiled = circuits
216+
217+
return transpiled

0 commit comments

Comments
 (0)