Skip to content

Commit

Permalink
Add BackendV2Converter class for treating BackendV1 as BackendV2 (#8759)
Browse files Browse the repository at this point in the history
* Add BackendV2Convert class for treating BackendV1 as BackendV2

This commit adds a new class BackendV2Converter which is a BackendV2
implementation that converts an input BackendV1 object into a BackendV2
implementation. This is useful for users that are supporting working
with arbitrary providers so that they can standardize on using the
newest access patterns even if a provider is still using BackendV1.
Similarly, for qiskit's internal usage, this gives us a path to move all
of the transpiler internals to use Target and avoid carrying around
duplicate information in the PassManagerConfig for passes that haven't
been updated. This will enable us to convert input BackendV1 instances
to a target once on initial calling and have the transpiler only ever
see a target.

Fixes #8611

* Update docstring

* Return empty options for _default_options

* Remove leftover pylint disable

* Expand standard gate conversions and handle missing basis_gates

* Fix copy paste error qubit_props_list_from_props() docstring

* Add name mapping argument to allow setting custom name-> gate mappings for conversion

* Add missing gamma parameter

* Fix handling of global ops in configuration

* Raise exception on custom gates without mapping

* Move name mapping to standard gates module

* Fix lint and docs

* Use gate object name attribute to build name mapping

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>

* Fix lint

* Fix pylint cyclic-import error

* Update qiskit/providers/backend_compat.py

Co-authored-by: Naoki Kanazawa <nkanazawa1989@gmail.com>

* Remove supported_instructions and add option for adding delay

The supported instructions field is underdocumented and it's not clear
that the instructions it contains are something that we want to support
directly. Since the only reason we added support for it in the target
generation was to handle the delay for ibm backends (mostly in tests as
the IBM provider will be using BackendV2 natively soon) this commit
removes its usage and adds a new flag for explicitly adding delay to the
backend's target.

* Add mention of converter class to API changes section

* Add missing flag usage to delay test

* Run black

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
Co-authored-by: Naoki Kanazawa <nkanazawa1989@gmail.com>
  • Loading branch information
3 people authored Sep 28, 2022
1 parent 7dbcb82 commit 4a857f6
Show file tree
Hide file tree
Showing 5 changed files with 406 additions and 1 deletion.
69 changes: 69 additions & 0 deletions qiskit/circuit/library/standard_gates/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,72 @@
from .z import ZGate, CZGate, CCZGate

from .multi_control_rotation_gates import mcrx, mcry, mcrz


def get_standard_gate_name_mapping():
"""Return a dictionary mapping the name of standard gates and instructions to an object for
that name."""
from qiskit.circuit.parameter import Parameter
from qiskit.circuit.measure import Measure
from qiskit.circuit.delay import Delay
from qiskit.circuit.reset import Reset

# Standard gates library mapping, multicontrolled gates not included since they're
# variable width
gates = [
IGate(),
SXGate(),
XGate(),
CXGate(),
RZGate(Parameter("λ")),
RGate(Parameter("ϴ"), Parameter("φ")),
Reset(),
C3SXGate(),
CCXGate(),
DCXGate(),
CHGate(),
CPhaseGate(Parameter("ϴ")),
CRXGate(Parameter("ϴ")),
CRYGate(Parameter("ϴ")),
CRZGate(Parameter("ϴ")),
CSwapGate(),
CSXGate(),
CUGate(Parameter("ϴ"), Parameter("φ"), Parameter("λ"), Parameter("γ")),
CU1Gate(Parameter("λ")),
CU3Gate(Parameter("ϴ"), Parameter("φ"), Parameter("λ")),
CYGate(),
CZGate(),
CCZGate(),
HGate(),
PhaseGate(Parameter("ϴ")),
RCCXGate(),
RC3XGate(),
RXGate(Parameter("ϴ")),
RXXGate(Parameter("ϴ")),
RYGate(Parameter("ϴ")),
RYYGate(Parameter("ϴ")),
RZZGate(Parameter("ϴ")),
RZXGate(Parameter("ϴ")),
XXMinusYYGate(Parameter("ϴ")),
XXPlusYYGate(Parameter("ϴ")),
ECRGate(),
SGate(),
SdgGate(),
CSGate(),
CSdgGate(),
SwapGate(),
iSwapGate(),
SXdgGate(),
TGate(),
TdgGate(),
UGate(Parameter("ϴ"), Parameter("φ"), Parameter("λ")),
U1Gate(Parameter("λ")),
U2Gate(Parameter("φ"), Parameter("λ")),
U3Gate(Parameter("ϴ"), Parameter("φ"), Parameter("λ")),
YGate(),
ZGate(),
Delay(Parameter("t")),
Measure(),
]
name_mapping = {gate.name: gate for gate in gates}
return name_mapping
7 changes: 7 additions & 0 deletions qiskit/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@
BackendV1
BackendV2
QubitProperties
BackendV2Converter
convert_to_target
Options
-------
Expand Down Expand Up @@ -666,6 +668,9 @@ def status(self):
operation on a given qubit is used to model the readout length. However, a
:obj:`~BackendV2` can implement multiple measurement types and list them
separately in a :class:`~qiskit.transpiler.Target`.
There is also a :class:`~.BackendV2Converter` class available that enables you
to wrap a :class:`~.BackendV1` object with a :class:`~.BackendV2` interface.
"""

import pkgutil
Expand All @@ -677,6 +682,8 @@ def status(self):
from qiskit.providers.backend import BackendV1
from qiskit.providers.backend import BackendV2
from qiskit.providers.backend import QubitProperties
from qiskit.providers.backend_compat import BackendV2Converter
from qiskit.providers.backend_compat import convert_to_target
from qiskit.providers.options import Options
from qiskit.providers.job import Job
from qiskit.providers.job import JobV1
Expand Down
282 changes: 282 additions & 0 deletions qiskit/providers/backend_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2020.
#
# 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.

"""Backend abstract interface for providers."""

from __future__ import annotations

from typing import List, Iterable, Any, Dict, Optional

from qiskit.exceptions import QiskitError

from qiskit.providers.backend import BackendV1, BackendV2
from qiskit.providers.backend import QubitProperties
from qiskit.utils.units import apply_prefix
from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping
from qiskit.circuit.measure import Measure
from qiskit.providers.models.backendconfiguration import BackendConfiguration
from qiskit.providers.models.backendproperties import BackendProperties
from qiskit.providers.models.pulsedefaults import PulseDefaults
from qiskit.providers.options import Options
from qiskit.providers.exceptions import BackendPropertyError


def convert_to_target(
configuration: BackendConfiguration,
properties: BackendProperties = None,
defaults: PulseDefaults = None,
custom_name_mapping: Optional[Dict[str, Any]] = None,
add_delay: bool = False,
):
"""Uses configuration, properties and pulse defaults
to construct and return Target class.
"""
# pylint: disable=cyclic-import
from qiskit.transpiler.target import (
Target,
InstructionProperties,
)

# Standard gates library mapping, multicontrolled gates not included since they're
# variable width
name_mapping = get_standard_gate_name_mapping()
target = None
if custom_name_mapping is not None:
name_mapping.update(custom_name_mapping)
# Parse from properties if it exsits
if properties is not None:
qubit_properties = qubit_props_list_from_props(properties=properties)
target = Target(num_qubits=configuration.n_qubits, qubit_properties=qubit_properties)
# Parse instructions
gates: Dict[str, Any] = {}
for gate in properties.gates:
name = gate.gate
if name in name_mapping:
if name not in gates:
gates[name] = {}
else:
raise QiskitError(
f"Operation name {name} does not have a known mapping. Use "
"custom_name_mapping to map this name to an Operation object"
)

qubits = tuple(gate.qubits)
gate_props = {}
for param in gate.parameters:
if param.name == "gate_error":
gate_props["error"] = param.value
if param.name == "gate_length":
gate_props["duration"] = apply_prefix(param.value, param.unit)
gates[name][qubits] = InstructionProperties(**gate_props)
for gate, props in gates.items():
inst = name_mapping[gate]
target.add_instruction(inst, props)
# Create measurement instructions:
measure_props = {}
for qubit, _ in enumerate(properties.qubits):
measure_props[(qubit,)] = InstructionProperties(
duration=properties.readout_length(qubit),
error=properties.readout_error(qubit),
)
target.add_instruction(Measure(), measure_props)
# Parse from configuration because properties doesn't exist
else:
target = Target(num_qubits=configuration.n_qubits)
for gate in configuration.gates:
name = gate.name
gate_props = (
{tuple(x): None for x in gate.coupling_map} # type: ignore[misc]
if hasattr(gate, "coupling_map")
else {None: None}
)
if name in name_mapping:
target.add_instruction(name_mapping[name], gate_props)
else:
raise QiskitError(
f"Operation name {name} does not have a known mapping. "
"Use custom_name_mapping to map this name to an Operation object"
)
target.add_instruction(Measure())
# parse global configuration properties
if hasattr(configuration, "dt"):
target.dt = configuration.dt
if hasattr(configuration, "timing_constraints"):
target.granularity = configuration.timing_constraints.get("granularity")
target.min_length = configuration.timing_constraints.get("min_length")
target.pulse_alignment = configuration.timing_constraints.get("pulse_alignment")
target.aquire_alignment = configuration.timing_constraints.get("acquire_alignment")
# If a pulse defaults exists use that as the source of truth
if defaults is not None:
inst_map = defaults.instruction_schedule_map
for inst in inst_map.instructions:
for qarg in inst_map.qubits_with_instruction(inst):
sched = inst_map.get(inst, qarg)
if inst in target:
try:
qarg = tuple(qarg)
except TypeError:
qarg = (qarg,)
if inst == "measure":
for qubit in qarg:
target[inst][(qubit,)].calibration = sched
elif qarg in target[inst]:
target[inst][qarg].calibration = sched
combined_global_ops = set()
if configuration.basis_gates:
combined_global_ops.update(configuration.basis_gates)
for op in combined_global_ops:
if op not in target:
if op in name_mapping:
target.add_instruction(
name_mapping[op], {(bit,): None for bit in range(target.num_qubits)}
)
else:
raise QiskitError(
f"Operation name '{op}' does not have a known mapping. Use "
"custom_name_mapping to map this name to an Operation object"
)
if add_delay and "delay" not in target:
target.add_instruction(
name_mapping["delay"], {(bit,): None for bit in range(target.num_qubits)}
)
return target


def qubit_props_list_from_props(
properties: BackendProperties,
) -> List[QubitProperties]:
"""Uses BackendProperties to construct
and return a list of QubitProperties.
"""
qubit_props: List[QubitProperties] = []
for qubit, _ in enumerate(properties.qubits):
try:
t_1 = properties.t1(qubit)
except BackendPropertyError:
t_1 = None
try:
t_2 = properties.t2(qubit)
except BackendPropertyError:
t_2 = None
try:
frequency = properties.frequency(qubit)
except BackendPropertyError:
frequency = None
qubit_props.append(
QubitProperties( # type: ignore[no-untyped-call]
t1=t_1,
t2=t_2,
frequency=frequency,
)
)
return qubit_props


class BackendV2Converter(BackendV2):
"""A converter class that takes a :class:`~.BackendV1` instance and wraps it in a
:class:`~.BackendV2` interface.
This class implements the :class:`~.BackendV2` interface and is used to enable
common access patterns between :class:`~.BackendV1` and :class:`~.BackendV2`. This
class should only be used if you need a :class:`~.BackendV2` and still need
compatibility with :class:`~.BackendV1`.
"""

def __init__(
self,
backend: BackendV1,
name_mapping: Optional[Dict[str, Any]] = None,
add_delay: bool = False,
):
"""Initialize a BackendV2 converter instance based on a BackendV1 instance.
Args:
backend: The input :class:`~.BackendV1` based backend to wrap in a
:class:`~.BackendV2` interface
name_mapping: An optional dictionary that maps custom gate/operation names in
``backend`` to an :class:`~.Operation` object representing that
gate/operation. By default most standard gates names are mapped to the
standard gate object from :mod:`qiskit.circuit.library` this only needs
to be specified if the input ``backend`` defines gates in names outside
that set.
add_delay: If set to true a :class:`~qiskit.circuit.Delay` operation
will be added to the target as a supported operation for all
qubits
"""
self._backend = backend
self._config = self._backend.configuration()
super().__init__(
provider=backend.provider,
name=backend.name(),
description=self._config.description,
online_date=self._config.online_date,
backend_version=self._config.backend_version,
)
self._options = self._backend._options
self._properties = None
if hasattr(self._backend, "properties"):
self._properties = self._backend.properties()
self._defaults = None
self._target = None
self._name_mapping = name_mapping
self._add_delay = add_delay

@property
def target(self):
"""A :class:`qiskit.transpiler.Target` object for the backend.
:rtype: Target
"""
if self._target is None:
if self._defaults is None and hasattr(self._backend, "defaults"):
self._defaults = self._backend.defaults()
if self._properties is None and hasattr(self._backend, "properties"):
self._properties = self._backend.properties()
self._target = convert_to_target(
self._config,
self._properties,
self._defaults,
custom_name_mapping=self._name_mapping,
add_delay=self._add_delay,
)
return self._target

@property
def max_circuits(self):
return self._config.max_experiments

@classmethod
def _default_options(cls):
return Options()

@property
def dtm(self) -> float:
return self._config.dtm

@property
def meas_map(self) -> List[List[int]]:
return self._config.dt

def drive_channel(self, qubit: int):
self._config.drive(qubit)

def measure_channel(self, qubit: int):
self._config.measure(qubit)

def acquire_channel(self, qubit: int):
self._config.acquire(qubit)

def control_channel(self, qubits: Iterable[int]):
self._config.control(qubits)

def run(self, run_input, **options):
return self._backend.run(run_input, **options)
15 changes: 15 additions & 0 deletions releasenotes/notes/backend-converter-05360f12f9042829.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
features:
- |
Added a new class, :class:`~BackendV2Converter`, which is used to wrap
a :class:`~.BackendV1` instance in a :class:`~.BackendV2` interface. It
enables you to have a :class:`~.BackendV2` instance from any
:class:`~.BackendV1`. This enables standardizing access patterns on the
newer :class:`~.BackendV2` interface even if you still support
:class:`~.BackendV1`.
- |
Added a new function :func:`~.convert_to_target` which is used to take
a :class:`~.BackendConfiguration`, and optionally a
:class:`~.BackendProperties` and :class:`~.PulseDefaults` and create
a :class:`~.Target` object equivalent to the contents of those objects.
Loading

0 comments on commit 4a857f6

Please sign in to comment.