Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BackendV2Converter class for treating BackendV1 as BackendV2 #8759

Merged
merged 25 commits into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4084f87
Add BackendV2Convert class for treating BackendV1 as BackendV2
mtreinish Sep 14, 2022
5e4b773
Merge branch 'main' into v1-to-v2-compat
mtreinish Sep 14, 2022
e724e29
Update docstring
mtreinish Sep 14, 2022
a51bea5
Merge remote-tracking branch 'origin/main' into v1-to-v2-compat
mtreinish Sep 20, 2022
9e8396e
Return empty options for _default_options
mtreinish Sep 20, 2022
2c8caa9
Remove leftover pylint disable
mtreinish Sep 20, 2022
c30bdd0
Expand standard gate conversions and handle missing basis_gates
mtreinish Sep 20, 2022
21d6c07
Fix copy paste error qubit_props_list_from_props() docstring
mtreinish Sep 20, 2022
7e8fe26
Add name mapping argument to allow setting custom name-> gate mapping…
mtreinish Sep 20, 2022
4b3bf2e
Add missing gamma parameter
mtreinish Sep 21, 2022
d4cb323
Fix handling of global ops in configuration
mtreinish Sep 21, 2022
86434cd
Raise exception on custom gates without mapping
mtreinish Sep 21, 2022
f2803ad
Move name mapping to standard gates module
mtreinish Sep 21, 2022
be7aeaf
Fix lint and docs
mtreinish Sep 21, 2022
bffe0f3
Use gate object name attribute to build name mapping
mtreinish Sep 21, 2022
aff11df
Fix lint
mtreinish Sep 21, 2022
d39cef0
Merge remote-tracking branch 'origin/main' into v1-to-v2-compat
mtreinish Sep 22, 2022
2635c4c
Fix pylint cyclic-import error
mtreinish Sep 22, 2022
25bd637
Update qiskit/providers/backend_compat.py
mtreinish Sep 27, 2022
301b5d0
Merge remote-tracking branch 'origin/main' into v1-to-v2-compat
mtreinish Sep 27, 2022
a4db666
Remove supported_instructions and add option for adding delay
mtreinish Sep 27, 2022
8cb7c12
Add mention of converter class to API changes section
mtreinish Sep 27, 2022
e3e266b
Add missing flag usage to delay test
mtreinish Sep 27, 2022
aeb9784
Run black
mtreinish Sep 27, 2022
bdfde4c
Merge branch 'main' into v1-to-v2-compat
mtreinish Sep 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
4 changes: 4 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 @@ -677,6 +679,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
279 changes: 279 additions & 0 deletions qiskit/providers/backend_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
# 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

# Some BackendConfiguration objects contain pulse only instructions in their
# supported_instructions field. Filter these out since they don't go in a
# TARGET
PULSE_INSTRUCTIONS = {"acquire", "shiftf", "setf", "play"}
mtreinish marked this conversation as resolved.
Show resolved Hide resolved


def convert_to_target(
configuration: BackendConfiguration,
properties: BackendProperties = None,
defaults: PulseDefaults = None,
custom_name_mapping: Optional[Dict[str, Any]] = None,
):
"""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:
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
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:
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
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")
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
# 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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, you can loop over target gates and check inst_map.has(...) and then add calibration.

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()
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
if configuration.basis_gates:
combined_global_ops.update(configuration.basis_gates)
if configuration.supported_instructions:
combined_global_ops.update(configuration.supported_instructions)
combined_global_ops -= PULSE_INSTRUCTIONS
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"
)
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] = []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annotating intermediate values with type hints? Who are you, and what have you done with Matthew?

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:
t_2 = None
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
qubit_props.append(
QubitProperties( # type: ignore[no-untyped-call]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, at this point there's no way you're the only author of this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, yeah I copied this code from the qiskit-ibm-provider which is based on an initial draft I wrote https://github.com/Qiskit/qiskit-ibm-provider/blob/main/qiskit_ibm_provider/utils/backend_converter.py and massaged a lot to pass mypy (which is run in the provider CI). But thanks for calling that out, I should add @rathishcholarajan and @kt474 to the co-authors list on this PR too since they did a bunch of work on the converter too.

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,
):
"""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.
"""
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

@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,
)
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