Skip to content

Commit 1ebfb0d

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

File tree

9 files changed

+235
-80
lines changed

9 files changed

+235
-80
lines changed

qiskit_experiments/calibration_management/base_calibration_experiment.py

Lines changed: 2 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,6 @@
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
@@ -198,20 +191,6 @@ def _default_experiment_options(cls) -> Options:
198191
options.update_options(result_index=-1, group="default")
199192
return options
200193

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-
215194
def update_calibrations(self, experiment_data: ExperimentData):
216195
"""Update parameter values in the :class:`.Calibrations` instance.
217196
@@ -295,42 +274,12 @@ def _transpiled_circuits(self) -> List[QuantumCircuit]:
295274
Returns:
296275
A list of transpiled circuits.
297276
"""
298-
transpiled = []
299-
for circ in self.circuits():
300-
circ = self._map_to_physical_qubits(circ)
277+
transpiled = super()._transpiled_circuits()
278+
for circ in transpiled:
301279
self._attach_calibrations(circ)
302280

303-
transpiled.append(circ)
304-
305281
return transpiled
306282

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-
334283
@abstractmethod
335284
def _attach_calibrations(self, circuit: QuantumCircuit):
336285
"""Attach the calibrations to the quantum circuit.

qiskit_experiments/framework/base_experiment.py

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
Base Experiment class.
1414
"""
1515

16-
from abc import ABC, abstractmethod
1716
import copy
17+
import importlib.metadata
18+
import logging
19+
from abc import ABC, abstractmethod
1820
from collections import OrderedDict
1921
from typing import Sequence, Optional, Tuple, List, Dict, Union
2022

@@ -25,12 +27,16 @@
2527
from qiskit.providers.options import Options
2628
from qiskit_experiments.framework import BackendData
2729
from qiskit_experiments.framework.store_init_args import StoreInitArgs
30+
from qiskit_experiments.framework.transpilation import check_transpilation_needed, map_qubits
2831
from qiskit_experiments.framework.base_analysis import BaseAnalysis
2932
from qiskit_experiments.framework.experiment_data import ExperimentData
3033
from qiskit_experiments.framework.configs import ExperimentConfig
3134
from qiskit_experiments.database_service import Qubit
3235

3336

37+
LOGGER = logging.getLogger(__name__)
38+
39+
3440
class BaseExperiment(ABC, StoreInitArgs):
3541
"""Abstract base class for experiments."""
3642

@@ -374,8 +380,35 @@ def _transpiled_circuits(self) -> List[QuantumCircuit]:
374380
This function can be overridden to define custom transpilation.
375381
"""
376382
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)
383+
384+
virtual_circuits = self.circuits()
385+
mapped_circuits = [map_qubits(c, self.physical_qubits) for c in virtual_circuits]
386+
387+
if "full_transpile" not in transpile_opts:
388+
LOGGER.debug(
389+
"Performing full transpile because base transpile options "
390+
"were overwritten and full_transpile was not specified."
391+
)
392+
full_transpile = True
393+
else:
394+
full_transpile = transpile_opts.pop("full_transpile", False)
395+
if not full_transpile and set(transpile_opts) - set(
396+
BaseExperiment._default_transpile_options()
397+
):
398+
# If an experiment specifies transpile options, it needs to go
399+
# through transpile()
400+
full_transpile = True
401+
LOGGER.debug(
402+
"Performing full transpile because non-default transpile options are specified."
403+
)
404+
405+
if not full_transpile:
406+
full_transpile = check_transpilation_needed(mapped_circuits, self.backend)
407+
408+
if full_transpile:
409+
transpiled = transpile(mapped_circuits, self.backend, **transpile_opts)
410+
else:
411+
transpiled = mapped_circuits
379412

380413
return transpiled
381414

@@ -418,11 +451,39 @@ def set_experiment_options(self, **fields):
418451

419452
@classmethod
420453
def _default_transpile_options(cls) -> Options:
421-
"""Default transpiler options for transpilation of circuits"""
454+
"""Default transpiler options for transpilation of circuits
455+
456+
Transpile Options:
457+
optimization_level (int): Optimization level to pass to
458+
:func:`qiskit.transpile`.
459+
num_processes (int): Number of processes to use during
460+
transpilation on Qiskit >= 1.0.
461+
full_transpile (bool): If ``True``,
462+
``BaseExperiment._transpiled_circuits`` (called by
463+
:meth:`BaseExperiment.run` if not overridden by a subclass)
464+
will call :func:`qiskit.transpile` on the output of
465+
:meth:`BaseExperiment.circuits` before executing the circuits.
466+
If ``False``, ``BaseExperiment._transpiled_circuits`` will
467+
reindex the qubits in the output of
468+
:meth:`BaseExperiment.circuits` using the experiments'
469+
:meth:`BaseExperiment.physical_qubits`. Then it will check if
470+
the circuit operations are all defined in the
471+
:class:`qiskit.transpiler.Target` of the experiment's backend
472+
or in the indiivdual circuit calibrations. If not, it will use
473+
:class:`qiskit.transpiler.passes.BasisTranslator` to map the
474+
circuit instructions to the backend. Additionally,
475+
the :class:`qiskit.transpiler.passes.PulseGates` transpiler
476+
pass will be run if the :class:`qiskit.transpiler.Target`
477+
contains any custom pulse gate calibrations.
478+
479+
"""
422480
# Experiment subclasses can override this method if they need
423481
# to set specific default transpiler options to transpile the
424482
# experiment circuits.
425-
return Options(optimization_level=0)
483+
opts = Options(optimization_level=0, full_transpile=False)
484+
if importlib.metadata.version("qiskit").partition(".")[0] != "0":
485+
opts["num_processes"] = 1
486+
return opts
426487

427488
@property
428489
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: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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+
from collections.abc import Sequence
19+
20+
from qiskit import QuantumCircuit, QuantumRegister
21+
from qiskit.exceptions import QiskitError
22+
from qiskit.providers import Backend
23+
from qiskit.pulse.calibration_entries import CalibrationPublisher
24+
from qiskit.transpiler import Target
25+
26+
27+
def map_qubits(
28+
circuit: QuantumCircuit,
29+
physical_qubits: Sequence[int],
30+
n_qubits: int | None = None,
31+
) -> QuantumCircuit:
32+
"""Generate a new version of a circuit with new qubit indices
33+
34+
This function iterates through the instructions of ``circuit`` and copies
35+
them into a new circuit with qubit indices replaced according to the
36+
entries in ``physical_qubits``. So qubit 0's instructions are applied to
37+
``physical_qubits[0]`` and qubit 1's to ``physical_qubits[1]``, etc.
38+
39+
This function behaves similarly to passing ``initial_layout`` to
40+
:func:`qiskit.transpile` but does not use a Qiskit
41+
:class:`~qiskit.transpiler.PassManager` and does not fill the circuit with
42+
ancillas.
43+
44+
Args:
45+
circuit: The :class:`~qiskit.QuantumCircuit` to re-index.
46+
physical_qubits: The list of new indices for ``circuit``'s qubit indices.
47+
n_qubits: Optional qubit size to use for the output circuit. If
48+
``None``, then the maximum of ``physical_qubits`` will be used.
49+
50+
Returns:
51+
The quantum circuit with new qubit indices
52+
"""
53+
if len(physical_qubits) != circuit.num_qubits:
54+
raise QiskitError(
55+
f"Circuit to map has {circuit.num_qubits} qubits, but "
56+
f"{len(physical_qubits)} physical qubits specified for mapping."
57+
)
58+
59+
# if all(p == r for p, r in zip(physical_qubits, range(circuit.num_qubits))):
60+
# # No mapping necessary
61+
# return circuit
62+
63+
circ_size = n_qubits if n_qubits is not None else (max(physical_qubits) + 1)
64+
p_qregs = QuantumRegister(circ_size)
65+
p_circ = QuantumCircuit(
66+
p_qregs,
67+
*circuit.cregs,
68+
name=circuit.name,
69+
metadata=circuit.metadata,
70+
global_phase=circuit.global_phase,
71+
)
72+
p_circ.compose(
73+
circuit,
74+
qubits=physical_qubits,
75+
inplace=True,
76+
copy=False,
77+
)
78+
return p_circ
79+
80+
81+
def _has_calibration(target: Target, name: str, qubits: tuple[int, ...]) -> bool:
82+
"""Wrapper to work around bug in Target.has_calibration"""
83+
try:
84+
has_cal = target.has_calibration(name, qubits)
85+
except AttributeError:
86+
has_cal = False
87+
88+
return has_cal
89+
90+
91+
def check_transpilation_needed(
92+
circuits: Sequence[QuantumCircuit],
93+
backend: Backend,
94+
) -> bool:
95+
"""Test if circuits are already compatible with backend
96+
97+
This function checks if circuits are able to be executed on ``backend``
98+
without transpilation. It loops through the circuits to check if any gate
99+
instructions are not included in the backend's
100+
:class:`~qiskit.transpiler.Target`. The :class:`~qiskit.transpiler.Target`
101+
is also checked for custom pulse gate calibrations for circuit's
102+
instructions. If all gates are included in the target and there are no
103+
custom calibrations, the function returns ``False`` indicating that
104+
transpilation is not needed.
105+
106+
This function returns ``True`` if the version of ``backend`` is less than
107+
2.
108+
109+
The motivation for this function is that when no transpilation is necessary
110+
it is faster to check the circuits in this way than to run
111+
:func:`~qiskit.transpile` and have it do nothing.
112+
113+
Args:
114+
circuits: The circuits to prepare for the backend.
115+
backend: The backend for which the circuits should be prepared.
116+
117+
Returns:
118+
``True`` if transpilation is needed. Otherwise, ``False``.
119+
"""
120+
transpilation_needed = False
121+
122+
if getattr(backend, "version", 0) <= 1:
123+
# Fall back to transpilation for BackendV1
124+
return True
125+
126+
target = backend.target
127+
128+
for circ in circuits:
129+
for inst in circ.data:
130+
if inst.operation.name == "barrier":
131+
continue
132+
qubits = tuple(circ.find_bit(q).index for q in inst.qubits)
133+
if not target.instruction_supported(inst.operation.name, qubits):
134+
transpilation_needed = True
135+
break
136+
if not circ.has_calibration_for(inst) and _has_calibration(
137+
target, inst.operation.name, qubits
138+
):
139+
cal = target.get_calibration(inst.operation.name, qubits, *inst.operation.params)
140+
if (
141+
cal.metadata.get("publisher", CalibrationPublisher.QISKIT)
142+
!= CalibrationPublisher.BACKEND_PROVIDER
143+
):
144+
transpilation_needed = True
145+
break
146+
if transpilation_needed:
147+
break
148+
149+
return transpilation_needed

qiskit_experiments/test/mock_iq_backend.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,10 @@ def run(self, run_input, **options):
135135

136136
self._compute_outcome_probabilities(run_input)
137137

138-
if run_input[0].num_qubits != 2:
138+
if run_input[0].num_qubits != 1:
139139
raise DataProcessorError(f"{self.__class__.__name__} is a two qubit mock device.")
140140

141-
prev_outcome, state_strings = "00", self._get_state_strings(2)
141+
prev_outcome, state_strings = "0", self._get_state_strings(1)
142142

143143
# Setup the list of dicts where each dict corresponds to a circuit.
144144
sorted_memory = [{"memory": [], "metadata": circ.metadata} for circ in run_input]
@@ -155,7 +155,7 @@ def run(self, run_input, **options):
155155

156156
for idx, circ in enumerate(run_input):
157157
counts = {}
158-
for key1, key2 in zip(["00", "01", "10", "11"], ["0x0", "0x1", "0x2", "0x3"]):
158+
for key1, key2 in zip(["0", "1"], ["0x0", "0x1"]):
159159
counts[key1] = sorted_memory[idx]["memory"].count(key2)
160160
run_result = {
161161
"shots": shots,
@@ -215,8 +215,8 @@ def _compute_outcome_probabilities(self, circuits: List[QuantumCircuit]):
215215
prob_1 = np.sin(angle / 2) ** 2
216216
prob_0 = 1 - prob_1
217217

218-
self._precomputed_probabilities[(idx, "00")] = [prob_0, prob_1, 0, 0]
219-
self._precomputed_probabilities[(idx, "01")] = [prob_1, prob_0, 0, 0]
218+
self._precomputed_probabilities[(idx, "0")] = [prob_0, prob_1]
219+
self._precomputed_probabilities[(idx, "1")] = [prob_1, prob_0]
220220

221221

222222
class MockIQBackend(FakeOpenPulse2QV2):

test/data_processing/test_restless_experiment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def test_end_to_end_restless_standard_processor(self, pi_ratio):
8282

8383
amp_exp = FineXAmplitude([0], backend)
8484
# standard data processor.
85-
standard_processor = DataProcessor("counts", [Probability("01")])
85+
standard_processor = DataProcessor("counts", [Probability("1")])
8686
amp_exp.analysis.set_options(data_processor=standard_processor)
8787
# enable a restless measurement setting.
8888
amp_exp.enable_restless(rep_delay=1e-6, override_processor_by_restless=False)

0 commit comments

Comments
 (0)