Skip to content

Commit

Permalink
Add fast transpilation method to BaseExperiment
Browse files Browse the repository at this point in the history
  • Loading branch information
wshanks committed Jun 6, 2024
1 parent d7df5f0 commit 662f19b
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,13 @@
from qiskit import QuantumCircuit
from qiskit.providers.options import Options
from qiskit.pulse import ScheduleBlock
from qiskit.transpiler import StagedPassManager, PassManager, Layout, CouplingMap
from qiskit.transpiler.passes import (
EnlargeWithAncilla,
FullAncillaAllocation,
ApplyLayout,
SetLayout,
)

from qiskit_experiments.calibration_management.calibrations import Calibrations
from qiskit_experiments.calibration_management.update_library import BaseUpdater
from qiskit_experiments.framework.base_analysis import BaseAnalysis
from qiskit_experiments.framework.base_experiment import BaseExperiment
from qiskit_experiments.framework.experiment_data import ExperimentData
from qiskit_experiments.framework.transpilation import map_qubits, minimal_transpile
from qiskit_experiments.exceptions import CalibrationError

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

@classmethod
def _default_transpile_options(cls) -> Options:
"""Return empty default transpile options as optimization_level is not used."""
return Options()

def set_transpile_options(self, **fields):
r"""Add a warning message.
.. note::
If your experiment has overridden `_transpiled_circuits` and needs
transpile options then please also override `set_transpile_options`.
"""
warnings.warn(f"Transpile options are not used in {self.__class__.__name__ }.")

def update_calibrations(self, experiment_data: ExperimentData):
"""Update parameter values in the :class:`.Calibrations` instance.
Expand Down Expand Up @@ -295,42 +275,13 @@ def _transpiled_circuits(self) -> List[QuantumCircuit]:
Returns:
A list of transpiled circuits.
"""
transpiled = []
for circ in self.circuits():
circ = self._map_to_physical_qubits(circ)
circuits = [map_qubits(c, self.physical_qubits) for c in self.circuits()]
for circ in circuits:
self._attach_calibrations(circ)

transpiled.append(circ)
transpiled = minimal_transpile(circuits, self.backend, self.transpile_options)

return transpiled

def _map_to_physical_qubits(self, circuit: QuantumCircuit) -> QuantumCircuit:
"""Map program qubits to physical qubits.
Args:
circuit: The quantum circuit to map to device qubits.
Returns:
A quantum circuit that has the same number of qubits as the backend and where
the physical qubits of the experiment have been properly mapped.
"""
initial_layout = Layout.from_intlist(list(self.physical_qubits), *circuit.qregs)

coupling_map = self._backend_data.coupling_map
if coupling_map is not None:
coupling_map = CouplingMap(self._backend_data.coupling_map)

layout = PassManager(
[
SetLayout(initial_layout),
FullAncillaAllocation(coupling_map),
EnlargeWithAncilla(),
ApplyLayout(),
]
)

return StagedPassManager(["layout"], layout=layout).run(circuit)

@abstractmethod
def _attach_calibrations(self, circuit: QuantumCircuit):
"""Attach the calibrations to the quantum circuit.
Expand Down
43 changes: 36 additions & 7 deletions qiskit_experiments/framework/base_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,23 @@
Base Experiment class.
"""

from abc import ABC, abstractmethod
import copy
from abc import ABC, abstractmethod
from collections import OrderedDict
from typing import Sequence, Optional, Tuple, List, Dict, Union

from qiskit import transpile, QuantumCircuit
from qiskit import QuantumCircuit
from qiskit.providers import Job, Backend
from qiskit.exceptions import QiskitError
from qiskit.qobj.utils import MeasLevel
from qiskit.providers.options import Options
from qiskit_experiments.framework import BackendData
from qiskit_experiments.framework.store_init_args import StoreInitArgs
from qiskit_experiments.framework.transpilation import (
DEFAULT_TRANSPILE_OPTIONS,
map_qubits,
minimal_transpile,
)
from qiskit_experiments.framework.base_analysis import BaseAnalysis
from qiskit_experiments.framework.experiment_data import ExperimentData
from qiskit_experiments.framework.configs import ExperimentConfig
Expand Down Expand Up @@ -373,9 +378,8 @@ def _transpiled_circuits(self) -> List[QuantumCircuit]:
This function can be overridden to define custom transpilation.
"""
transpile_opts = copy.copy(self.transpile_options.__dict__)
transpile_opts["initial_layout"] = list(self.physical_qubits)
transpiled = transpile(self.circuits(), self.backend, **transpile_opts)
circuits = [map_qubits(c, self.physical_qubits) for c in self.circuits()]
transpiled = minimal_transpile(circuits, self.backend, self.transpile_options)

return transpiled

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

@classmethod
def _default_transpile_options(cls) -> Options:
"""Default transpiler options for transpilation of circuits"""
"""Default transpiler options for transpilation of circuits
Transpile Options:
optimization_level (int): Optimization level to pass to
:func:`qiskit.transpile`.
num_processes (int): Number of processes to use during
transpilation on Qiskit >= 1.0.
full_transpile (bool): If ``True``,
``BaseExperiment._transpiled_circuits`` (called by
:meth:`BaseExperiment.run` if not overridden by a subclass)
will call :func:`qiskit.transpile` on the output of
:meth:`BaseExperiment.circuits` before executing the circuits.
If ``False``, ``BaseExperiment._transpiled_circuits`` will
reindex the qubits in the output of
:meth:`BaseExperiment.circuits` using the experiments'
:meth:`BaseExperiment.physical_qubits`. Then it will check if
the circuit operations are all defined in the
:class:`qiskit.transpiler.Target` of the experiment's backend
or in the indiivdual circuit calibrations. If not, it will use
:class:`qiskit.transpiler.passes.BasisTranslator` to map the
circuit instructions to the backend. Additionally,
the :class:`qiskit.transpiler.passes.PulseGates` transpiler
pass will be run if the :class:`qiskit.transpiler.Target`
contains any custom pulse gate calibrations.
"""
# Experiment subclasses can override this method if they need
# to set specific default transpiler options to transpile the
# experiment circuits.
return Options(optimization_level=0)
return copy.copy(DEFAULT_TRANSPILE_OPTIONS)

@property
def transpile_options(self) -> Options:
Expand Down
9 changes: 4 additions & 5 deletions qiskit_experiments/framework/experiment_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -866,11 +866,10 @@ def _add_job_data(
LOG.warning("Job was cancelled before completion [Job ID: %s]", jid)
return jid, False
if status == JobStatus.ERROR:
LOG.error(
"Job data not added for errored job [Job ID: %s]\nError message: %s",
jid,
job.error_message(),
)
msg = f"Job data not added for errored job [Job ID: {jid}]"
if hasattr(job, "error_message"):
msg += f"\nError message: {job.error_message()}"
LOG.error(msg)
return jid, False
LOG.warning("Adding data from job failed [Job ID: %s]", job.job_id())
raise ex
Expand Down
217 changes: 217 additions & 0 deletions qiskit_experiments/framework/transpilation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""
Functions for preparing circuits for execution
"""

from __future__ import annotations

import importlib.metadata
import logging
from collections.abc import Sequence

from qiskit import QuantumCircuit, QuantumRegister, transpile
from qiskit.exceptions import QiskitError
from qiskit.providers import Backend
from qiskit.providers.options import Options
from qiskit.pulse.calibration_entries import CalibrationPublisher
from qiskit.transpiler import Target


LOGGER = logging.getLogger(__file__)

DEFAULT_TRANSPILE_OPTIONS = Options(optimization_level=0, full_transpile=False)
if importlib.metadata.version("qiskit").partition(".")[0] != "0":
DEFAULT_TRANSPILE_OPTIONS["num_processes"] = 1


def map_qubits(
circuit: QuantumCircuit,
physical_qubits: Sequence[int],
n_qubits: int | None = None,
) -> QuantumCircuit:
"""Generate a new version of a circuit with new qubit indices
This function iterates through the instructions of ``circuit`` and copies
them into a new circuit with qubit indices replaced according to the
entries in ``physical_qubits``. So qubit 0's instructions are applied to
``physical_qubits[0]`` and qubit 1's to ``physical_qubits[1]``, etc.
This function behaves similarly to passing ``initial_layout`` to
:func:`qiskit.transpile` but does not use a Qiskit
:class:`~qiskit.transpiler.PassManager` and does not fill the circuit with
ancillas.
Args:
circuit: The :class:`~qiskit.QuantumCircuit` to re-index.
physical_qubits: The list of new indices for ``circuit``'s qubit indices.
n_qubits: Optional qubit size to use for the output circuit. If
``None``, then the maximum of ``physical_qubits`` will be used.
Returns:
The quantum circuit with new qubit indices
"""
if len(physical_qubits) != circuit.num_qubits:
raise QiskitError(
f"Circuit to map has {circuit.num_qubits} qubits, but "
f"{len(physical_qubits)} physical qubits specified for mapping."
)

# if all(p == r for p, r in zip(physical_qubits, range(circuit.num_qubits))):
# # No mapping necessary
# return circuit

circ_size = n_qubits if n_qubits is not None else (max(physical_qubits) + 1)
p_qregs = QuantumRegister(circ_size)
p_circ = QuantumCircuit(
p_qregs,
*circuit.cregs,
name=circuit.name,
metadata=circuit.metadata,
global_phase=circuit.global_phase,
)
p_circ.compose(
circuit,
qubits=physical_qubits,
inplace=True,
copy=False,
)
return p_circ


def _has_calibration(target: Target, name: str, qubits: tuple[int, ...]) -> bool:
"""Wrapper to work around bug in Target.has_calibration"""
try:
has_cal = target.has_calibration(name, qubits)
except AttributeError:
has_cal = False

return has_cal


def check_transpilation_needed(
circuits: Sequence[QuantumCircuit],
backend: Backend,
) -> bool:
"""Test if circuits are already compatible with backend
This function checks if circuits are able to be executed on ``backend``
without transpilation. It loops through the circuits to check if any gate
instructions are not included in the backend's
:class:`~qiskit.transpiler.Target`. The :class:`~qiskit.transpiler.Target`
is also checked for custom pulse gate calibrations for circuit's
instructions. If all gates are included in the target and there are no
custom calibrations, the function returns ``False`` indicating that
transpilation is not needed.
This function returns ``True`` if the version of ``backend`` is less than
2.
The motivation for this function is that when no transpilation is necessary
it is faster to check the circuits in this way than to run
:func:`~qiskit.transpile` and have it do nothing.
Args:
circuits: The circuits to prepare for the backend.
backend: The backend for which the circuits should be prepared.
Returns:
``True`` if transpilation is needed. Otherwise, ``False``.
"""
transpilation_needed = False

if getattr(backend, "version", 0) <= 1:
# Fall back to transpilation for BackendV1
return True

target = backend.target

for circ in circuits:
for inst in circ.data:
if inst.operation.name == "barrier" or circ.has_calibration_for(inst):
continue
qubits = tuple(circ.find_bit(q).index for q in inst.qubits)
if not target.instruction_supported(inst.operation.name, qubits):
transpilation_needed = True
break
if _has_calibration(target, inst.operation.name, qubits):
cal = target.get_calibration(inst.operation.name, qubits, *inst.operation.params)
if (
cal.metadata.get("publisher", CalibrationPublisher.QISKIT)
!= CalibrationPublisher.BACKEND_PROVIDER
):
transpilation_needed = True
break
if transpilation_needed:
break

return transpilation_needed


def minimal_transpile(
circuits: Sequence[QuantumCircuit],
backend: Backend,
options: Options,
) -> list[QuantumCircuit]:
"""Prepare circuits for execution on a backend
This function is a wrapper around :func:`~qiskit.transpile` to prepare
circuits for execution ``backend`` that tries to do less work in the case
in which the ``circuits`` can already be executed on the backend without
modification.
The instructions in ``circuits`` are checked to see if they can be executed
by the ``backend`` using :func:`check_transpilation_needed`. If the
circuits can not be executed, :func:`~qiskit.transpile` is called on them.
``options`` is a set of options to pass to the :func:`~qiskit.transpile`
(see detailed description of ``options``). The special ``full_transpile``
option can also be set to ``True`` to force calling
:func:`~qiskit.transpile`.
Args:
circuits: The circuits to prepare for the backend.
backend: The backend for which the circuits should be prepared.
options: Options for the transpilation. ``full_transpile`` can be set
to ``True`` to force this function to pass the circuits to
:func:`~qiskit.transpile`. Other options are passed as arguments to
:func:`qiskit.transpile` if it is called.
Returns:
The prepared circuits
"""
options = dict(options.items())

if "full_transpile" not in options:
LOGGER.debug(
"Performing full transpile because base transpile options "
"were overwritten and full_transpile was not specified."
)
full_transpile = True
else:
full_transpile = options.pop("full_transpile", False)
if not full_transpile and set(options) - set(DEFAULT_TRANSPILE_OPTIONS):
# If an experiment specifies transpile options, it needs to go
# through transpile()
full_transpile = True
LOGGER.debug(
"Performing full transpile because non-default transpile options are specified."
)

if not full_transpile:
full_transpile = check_transpilation_needed(circuits, backend)

if full_transpile:
transpiled = transpile(circuits, backend, **options)
else:
transpiled = circuits

return transpiled
Loading

0 comments on commit 662f19b

Please sign in to comment.