Repository: Qiskit/qiskit

Issue:
Compatibility classes for converting BackendV1 to BackendV2 to facilitate migration
### What should we add?

For package developers trying to transition to new backends it would be very convenient if terra had wrapper backend classes/functions that can make a backend V1 into a backend V2 and vice versa.

Something to this effect was [recently added to qiskit-experiments](https://github.com/Qiskit/qiskit-experiments/blob/main/qiskit_experiments/framework/backend_data.py#L23) to help facilitate handling both types of backends in tests, however it falls a bit short of general use because it uses duck-typing of methods for both backends but wont actually be an instance of a backend so can't be used as a drop-in replacement.

It would be convenient having either a backend class or generator function that can take one backend and return an equivalent instance of the other.


Eg we could have classes that can be initialized with another backend like:
```python
class WrappedBackendV1(BackendV1):
    def __init__(self, backend: Union[BackendV1, BackendV2]):

class WrappedBackendV2(BackendV2):
    def __init__(self, backend: Union[BackendV1, BackendV2]):
```

An alternative would be a function to generate a backend either V1->V2 or V2->V1 (or trivially V1->V1, V2->V2)

```python
# function interface
def wrapped_backend(
    backend: Union[BackendV1, BackendV2], target_cls=BackendV2
) -> Union[BackendV1, BackendV2]
```



Discussion:
From a typing perspective I don't like your proposed interfaces haha (`target_cls` implies `wrapped_backend` lives in a dependent type system, and are the unions in `WrappedBackend` meant to keep expanding with every new spec, so the classes just get more and more `if`s?), but the conversion functionality seems like a sensible addition to help migration.  I'm not convinced that we can make a sensible `BackendV2 -> BackendV1` converter, because `BackendV2` contains much more precise information about various `Target` properties that can't be represented in a `BackendV1`, but we don't want people to keep using `BackendV1` anyway, and we wouldn't provide "backwards migration" for any other new feature.

Since I complained about your proposed interface, my suggestion would be to add a single class method `BackendV2.from_v1(cls, base: BackendV1) -> BackendV2`.  It's got stricter/safer typing, and it's read as an associated constructor of the `BackendV2` class.  Internally it'd probably return an instance of some private `_ProxyBackendV1AsV2` type.
As for the specific details of building this most of the code already exists in the fake providers already. The majority of such a compatibility interface would be converting `BackendConfiguration`, `BackendProperties`, and `PulseDefaults` into a `Target` and assorted other instance attributes. The fake providers (plus qiskit-ibm-provider and qiskit-ibm-runtime) took some draft code I wrote for them to do that conversion several months ago to build the same converter. So having I think both potentially promoting https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/providers/fake_provider/utils/backend_converter.py as more public (for external consumption in other `*ibm*` providers) and using that to build the interface shouldn't be very difficult.

Resolving patch:
diff --git a/qiskit/circuit/library/standard_gates/__init__.py b/qiskit/circuit/library/standard_gates/__init__.py
--- a/qiskit/circuit/library/standard_gates/__init__.py
+++ b/qiskit/circuit/library/standard_gates/__init__.py
@@ -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
diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py
--- a/qiskit/providers/__init__.py
+++ b/qiskit/providers/__init__.py
@@ -94,6 +94,8 @@
    BackendV1
    BackendV2
    QubitProperties
+   BackendV2Converter
+   convert_to_target
 
 Options
 -------
@@ -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
@@ -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
diff --git a/qiskit/providers/backend_compat.py b/qiskit/providers/backend_compat.py
new file mode 100644
--- /dev/null
+++ b/qiskit/providers/backend_compat.py
@@ -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)


Test patch:
diff --git a/test/python/providers/test_fake_backends.py b/test/python/providers/test_fake_backends.py
--- a/test/python/providers/test_fake_backends.py
+++ b/test/python/providers/test_fake_backends.py
@@ -24,7 +24,15 @@
 from qiskit.exceptions import QiskitError
 from qiskit.execute_function import execute
 from qiskit.test.base import QiskitTestCase
-from qiskit.providers.fake_provider import FakeProviderForBackendV2, FakeProvider, FakeMumbaiV2
+from qiskit.providers.fake_provider import (
+    FakeProviderForBackendV2,
+    FakeProvider,
+    FakeMumbaiV2,
+    FakeYorktown,
+    FakeMumbai,
+)
+from qiskit.providers.backend_compat import BackendV2Converter
+from qiskit.providers.backend import BackendV2
 from qiskit.utils import optionals
 
 FAKE_PROVIDER_FOR_BACKEND_V2 = FakeProviderForBackendV2()
@@ -161,3 +169,27 @@ def test_delay_circuit(self):
         qc.measure_all()
         res = transpile(qc, backend)
         self.assertIn("delay", res.count_ops())
+
+    @data(0, 1, 2, 3)
+    def test_converter(self, opt_level):
+        backend = FakeYorktown()
+        backend_v2 = BackendV2Converter(backend)
+        self.assertIsInstance(backend_v2, BackendV2)
+        res = transpile(self.circuit, backend_v2, optimization_level=opt_level)
+        job = backend_v2.run(res)
+        result = job.result()
+        counts = result.get_counts()
+        max_count = max(counts.items(), key=operator.itemgetter(1))[0]
+        self.assertEqual(max_count, "11")
+
+    def test_converter_delay_circuit(self):
+        backend = FakeMumbai()
+        backend_v2 = BackendV2Converter(backend, add_delay=True)
+        self.assertIsInstance(backend_v2, BackendV2)
+        qc = QuantumCircuit(2)
+        qc.delay(502, 0, unit="ns")
+        qc.x(1)
+        qc.delay(250, 1, unit="ns")
+        qc.measure_all()
+        res = transpile(qc, backend_v2)
+        self.assertIn("delay", res.count_ops())