diff --git a/qiskit/circuit/library/standard_gates/__init__.py b/qiskit/circuit/library/standard_gates/__init__.py index d1345e13d8f2..3dca1abb6201 100644 --- 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 index e83b145b2f5a..e96d5635fe00 100644 --- 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 index 000000000000..60889ff22c70 --- /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) diff --git a/releasenotes/notes/backend-converter-05360f12f9042829.yaml b/releasenotes/notes/backend-converter-05360f12f9042829.yaml new file mode 100644 index 000000000000..97695b55d084 --- /dev/null +++ b/releasenotes/notes/backend-converter-05360f12f9042829.yaml @@ -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. diff --git a/test/python/providers/test_fake_backends.py b/test/python/providers/test_fake_backends.py index 79dfc27121fd..5c0da0f35dbf 100644 --- 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())