diff --git a/qiskit/compiler/scheduler.py b/qiskit/compiler/scheduler.py index c4baf6c2f0cb..56ddc52b2bc9 100644 --- a/qiskit/compiler/scheduler.py +++ b/qiskit/compiler/scheduler.py @@ -66,24 +66,34 @@ def schedule( QiskitError: If ``inst_map`` and ``meas_map`` are not passed and ``backend`` is not passed """ start_time = time() - if inst_map is None: - if backend is None: - raise QiskitError( - "Must supply either a backend or InstructionScheduleMap for scheduling passes." - ) - defaults = backend.defaults() - if defaults is None: - raise QiskitError( - "The backend defaults are unavailable. The backend may not support pulse." - ) - inst_map = defaults.instruction_schedule_map - if meas_map is None: - if backend is None: - raise QiskitError("Must supply either a backend or a meas_map for scheduling passes.") - meas_map = backend.configuration().meas_map - if dt is None: - if backend is not None: - dt = backend.configuration().dt + if backend and getattr(backend, "version", 0) > 1: + if inst_map is None: + inst_map = backend.instruction_schedule_map + if meas_map is None: + meas_map = backend.meas_map + if dt is None: + dt = backend.dt + else: + if inst_map is None: + if backend is None: + raise QiskitError( + "Must supply either a backend or InstructionScheduleMap for scheduling passes." + ) + defaults = backend.defaults() + if defaults is None: + raise QiskitError( + "The backend defaults are unavailable. The backend may not support pulse." + ) + inst_map = defaults.instruction_schedule_map + if meas_map is None: + if backend is None: + raise QiskitError( + "Must supply either a backend or a meas_map for scheduling passes." + ) + meas_map = backend.configuration().meas_map + if dt is None: + if backend is not None: + dt = backend.configuration().dt schedule_config = ScheduleConfig(inst_map=inst_map, meas_map=meas_map, dt=dt) circuits = circuits if isinstance(circuits, list) else [circuits] diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index c3ace64350ab..342195921c38 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -352,8 +352,15 @@ def _check_circuits_coupling_map(circuits, transpile_args, backend): max_qubits = parsed_coupling_map.size() # If coupling_map is None, the limit might be in the backend (like in 1Q devices) - elif backend is not None and not backend.configuration().simulator: - max_qubits = backend.configuration().n_qubits + elif backend is not None: + backend_version = getattr(backend, "version", 0) + if not isinstance(backend_version, int): + backend_version = 0 + if backend_version <= 1: + if not backend.configuration().simulator: + max_qubits = backend.configuration().n_qubits + else: + max_qubits = backend.num_qubits if max_qubits is not None and (num_qubits > max_qubits): raise TranspilerError( @@ -608,6 +615,11 @@ def _create_faulty_qubits_map(backend): from working qubit in the backend to dummy qubits that are consecutive and connected.""" faulty_qubits_map = None if backend is not None: + backend_version = getattr(backend, "version", 0) + if not isinstance(backend_version, int): + backend_version = 0 + if backend_version > 1: + return None if backend.properties(): faulty_qubits = backend.properties().faulty_qubits() faulty_edges = [gates.qubits for gates in backend.properties().faulty_gates()] @@ -639,8 +651,14 @@ def _create_faulty_qubits_map(backend): def _parse_basis_gates(basis_gates, backend, circuits): # try getting basis_gates from user, else backend if basis_gates is None: - if getattr(backend, "configuration", None): - basis_gates = getattr(backend.configuration(), "basis_gates", None) + backend_version = getattr(backend, "version", 0) + if not isinstance(backend_version, int): + backend_version = 0 + if backend_version <= 1: + if getattr(backend, "configuration", None): + basis_gates = getattr(backend.configuration(), "basis_gates", None) + else: + basis_gates = backend.operation_names # basis_gates could be None, or a list of basis, e.g. ['u3', 'cx'] if basis_gates is None or ( isinstance(basis_gates, list) and all(isinstance(i, str) for i in basis_gates) @@ -666,28 +684,34 @@ def _parse_inst_map(inst_map, backend, num_circuits): def _parse_coupling_map(coupling_map, backend, num_circuits): # try getting coupling_map from user, else backend if coupling_map is None: - if getattr(backend, "configuration", None): - configuration = backend.configuration() - if hasattr(configuration, "coupling_map") and configuration.coupling_map: - faulty_map = _create_faulty_qubits_map(backend) - if faulty_map: - faulty_edges = [gate.qubits for gate in backend.properties().faulty_gates()] - functional_gates = [ - edge for edge in configuration.coupling_map if edge not in faulty_edges - ] - coupling_map = CouplingMap() - for qubit1, qubit2 in functional_gates: - if faulty_map[qubit1] is not None and faulty_map[qubit2] is not None: - coupling_map.add_edge(faulty_map[qubit1], faulty_map[qubit2]) - if configuration.n_qubits != coupling_map.size(): - warnings.warn( - "The backend has currently some qubits/edges out of service." - " This temporarily reduces the backend size from " - f"{configuration.n_qubits} to {coupling_map.size()}", - UserWarning, - ) - else: - coupling_map = CouplingMap(configuration.coupling_map) + backend_version = getattr(backend, "version", 0) + if not isinstance(backend_version, int): + backend_version = 0 + if backend_version <= 1: + if getattr(backend, "configuration", None): + configuration = backend.configuration() + if hasattr(configuration, "coupling_map") and configuration.coupling_map: + faulty_map = _create_faulty_qubits_map(backend) + if faulty_map: + faulty_edges = [gate.qubits for gate in backend.properties().faulty_gates()] + functional_gates = [ + edge for edge in configuration.coupling_map if edge not in faulty_edges + ] + coupling_map = CouplingMap() + for qubit1, qubit2 in functional_gates: + if faulty_map[qubit1] is not None and faulty_map[qubit2] is not None: + coupling_map.add_edge(faulty_map[qubit1], faulty_map[qubit2]) + if configuration.n_qubits != coupling_map.size(): + warnings.warn( + "The backend has currently some qubits/edges out of service." + " This temporarily reduces the backend size from " + f"{configuration.n_qubits} to {coupling_map.size()}", + UserWarning, + ) + else: + coupling_map = CouplingMap(configuration.coupling_map) + else: + coupling_map = backend.coupling_map # coupling_map could be None, or a list of lists, e.g. [[0, 1], [2, 1]] if coupling_map is None or isinstance(coupling_map, CouplingMap): @@ -743,10 +767,22 @@ def _parse_backend_num_qubits(backend, num_circuits): if backend is None: return [None] * num_circuits if not isinstance(backend, list): - return [backend.configuration().n_qubits] * num_circuits + backend_version = getattr(backend, "version", 0) + if not isinstance(backend_version, int): + backend_version = 0 + if backend_version <= 1: + return [backend.configuration().n_qubits] * num_circuits + else: + return [backend.num_qubits] * num_circuits backend_num_qubits = [] for a_backend in backend: - backend_num_qubits.append(a_backend.configuration().n_qubits) + backend_version = getattr(backend, "version", 0) + if not isinstance(backend_version, int): + backend_version = 0 + if backend_version <= 1: + backend_num_qubits.append(a_backend.configuration().n_qubits) + else: + backend_num_qubits.append(a_backend.num_qubits) return backend_num_qubits @@ -938,11 +974,16 @@ def _parse_timing_constraints(backend, timing_constraints, num_circuits): if backend is None and timing_constraints is None: timing_constraints = TimingConstraints() else: - if timing_constraints is None: - # get constraints from backend - timing_constraints = getattr(backend.configuration(), "timing_constraints", {}) - timing_constraints = TimingConstraints(**timing_constraints) - + backend_version = getattr(backend, "version", 0) + if not isinstance(backend_version, int): + backend_version = 0 + if backend_version <= 1: + if timing_constraints is None: + # get constraints from backend + timing_constraints = getattr(backend.configuration(), "timing_constraints", {}) + timing_constraints = TimingConstraints(**timing_constraints) + else: + timing_constraints = backend.target.timing_constraints() return [timing_constraints] * num_circuits diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index b1cb4344721d..0aa3aadacc32 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -92,6 +92,8 @@ Backend BackendV1 + BackendV2 + QubitProperties Options ------- @@ -547,6 +549,8 @@ def status(self): from qiskit.providers.provider import ProviderV1 from qiskit.providers.backend import Backend from qiskit.providers.backend import BackendV1 +from qiskit.providers.backend import BackendV2 +from qiskit.providers.backend import QubitProperties from qiskit.providers.options import Options from qiskit.providers.job import Job from qiskit.providers.job import JobV1 diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index b1c7189d247e..6034d5106c9f 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -10,13 +10,23 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +# pylint: disable=invalid-name + """Backend abstract interface for providers.""" from abc import ABC from abc import abstractmethod +from collections import defaultdict +import datetime +import logging +from typing import List, Union, Iterable, Tuple +from qiskit.providers.provider import Provider from qiskit.providers.models.backendstatus import BackendStatus +from qiskit.circuit.gate import Instruction + +logger = logging.getLogger(__name__) class Backend: @@ -229,3 +239,385 @@ class can handle either situation. Job: The job object for the run """ pass + + +class QubitProperties: + """A representation of the properties of a qubit on a backend. + + This class provides the optional properties that a backend can provide for + a qubit. These represent the set of qubit properties that Qiskit can + currently work with if present. However if your backend provides additional + properties of qubits you should subclass this to add additional custom + attributes for those custom/additional properties provided by the backend. + """ + + __slots__ = ("t1", "t2", "frequency") + + def __init__(self, t1=None, t2=None, frequency=None): + """Create a new ``QubitProperties`` object + + Args: + t1: The T1 time for a qubit in seconds + t2: The T2 time for a qubit in seconds + frequency: The frequency of a qubit in Hz + """ + self.t1 = t1 + self.t2 = t2 + self.frequency = frequency + + def __repr__(self): + return f"QubitProperties(t1={self.t1}, t2={self.t2}, " f"frequency={self.frequency})" + + +class BackendV2(Backend, ABC): + """Abstract class for Backends + + This abstract class is to be used for all Backend objects created by a + provider. This version differs from earlier abstract Backend classes in + that the configuration attribute no longer exists. Instead, attributes + exposing equivalent required immutable properties of the backend device + are added. For example ``backend.configuration().n_qubits`` is accessible + from ``backend.num_qubits`` now. + + The ``options`` attribute of the backend is used to contain the dynamic + user configurable options of the backend. It should be used more for + runtime options that configure how the backend is used. For example, + something like a ``shots`` field for a backend that runs experiments which + would contain an int for how many shots to execute. + + If migrating a provider from :class:`~qiskit.providers.BackendV1` or + :class:`~qiskit.providers.BaseBackend` one thing to keep in mind is for + backwards compatibility you might need to add a configuration method that + will build a :class:`~qiskit.providers.models.BackendConfiguration` object + and :class:`~qiskit.providers.models.BackendProperties` from the attributes + defined in this class for backwards compatibility. + """ + + version = 2 + + def __init__( + self, + provider: Provider = None, + name: str = None, + description: str = None, + online_date: datetime.datetime = None, + backend_version: str = None, + **fields, + ): + """Initialize a BackendV2 based backend + + Args: + provider: An optional backwards reference to the + :class:`~qiskit.providers.Provider` object that the backend + is from + name: An optional name for the backend + description: An optional description of the backend + online_date: An optional datetime the backend was brought online + backend_version: An optional backend version string. This differs + from the :attr:`~qiskit.providers.BackendV2.version` attribute + as :attr:`~qiskit.providers.BackendV2.version` is for the + abstract :attr:`~qiskit.providers.Backend` abstract interface + version of the object while ``backend_version`` is for + versioning the backend itself. + fields: kwargs for the values to use to override the default + options. + + Raises: + AttributeError: If a field is specified that's outside the backend's + options + """ + + self._options = self._default_options() + self._provider = provider + if fields: + for field in fields: + if field not in self._options.data: + raise AttributeError("Options field %s is not valid for this backend" % field) + self._options.update_config(**fields) + self._basis_gates_all = None + self.name = name + self.description = description + self.online_date = online_date + self.backend_version = backend_version + + @property + def instructions(self) -> List[Tuple[Instruction, Tuple[int]]]: + """A list of Instruction tuples on the backend of the form ``(instruction, (qubits)``""" + return self.target.instructions + + @property + def operations(self) -> List[Instruction]: + """A list of :class:`~qiskit.circuit.Instruction` instances that the backend supports.""" + return list(self.target.operations) + + def _compute_non_global_basis(self): + incomplete_basis_gates = [] + size_dict = defaultdict(int) + size_dict[1] = self.target.num_qubits + for qarg in self.target.qargs: + if len(qarg) == 1: + continue + size_dict[len(qarg)] += 1 + for inst, qargs in self.target.items(): + qarg_sample = next(iter(qargs)) + if qarg_sample is None: + continue + if len(qargs) != size_dict[len(qarg_sample)]: + incomplete_basis_gates.append(inst) + self._basis_gates_all = incomplete_basis_gates + + @property + def operation_names(self) -> List[str]: + """A list of instruction names that the backend supports.""" + if self._basis_gates_all is None: + self._compute_non_global_basis() + if self._basis_gates_all: + invalid_str = ",".join(self._basis_gates_all) + msg = ( + f"This backend's operations: {invalid_str} only apply to a subset of " + "qubits. Using this property to get 'basis_gates' for the " + "transpiler may potentially create invalid output" + ) + logger.warning(msg) + return list(self.target.operation_names) + + @property + @abstractmethod + def target(self): + """A :class:`qiskit.transpiler.Target` object for the backend. + + :rtype: Target + """ + pass + + @property + def num_qubits(self) -> int: + """Return the number of qubits the backend has.""" + return self.target.num_qubits + + @property + def coupling_map(self): + """Return the :class:`~qiskit.transpiler.CouplingMap` object""" + return self.target.build_coupling_map() + + @property + def instruction_durations(self): + """Return the :class:`~qiskit.transpiler.InstructionDurations` object.""" + return self.target.durations() + + @property + @abstractmethod + def max_circuits(self): + """The maximum number of circuits (or Pulse schedules) that can be + run in a single job. + + If there is no limit this will return None + """ + pass + + @classmethod + @abstractmethod + def _default_options(cls): + """Return the default options + + This method will return a :class:`qiskit.providers.Options` + subclass object that will be used for the default options. These + should be the default parameters to use for the options of the + backend. + + Returns: + qiskit.providers.Options: A options object with + default values set + """ + pass + + @property + def dt(self) -> Union[float, None]: + """Return the system time resolution of input signals + + This is required to be implemented if the backend supports Pulse + scheduling. + + Returns: + dt: The input signal timestep in seconds. If the backend doesn't + define ``dt`` ``None`` will be returned + """ + return self.target.dt + + @property + def dtm(self) -> float: + """Return the system time resolution of output signals + + Returns: + dtm: The output signal timestep in seconds. + + Raises: + NotImplementedError: if the backend doesn't support querying the + output signal timestep + """ + raise NotImplementedError + + @property + def meas_map(self) -> List[List[int]]: + """Return the grouping of measurements which are multiplexed + + This is required to be implemented if the backend supports Pulse + scheduling. + + Returns: + meas_map: The grouping of measurements which are multiplexed + + Raises: + NotImplementedError: if the backend doesn't support querying the + measurement mapping + """ + raise NotImplementedError + + @property + def instruction_schedule_map(self): + """Return the :class:`~qiskit.pulse.InstructionScheduleMap` for the + instructions defined in this backend's target.""" + return self.target.instruction_schedule_map() + + def qubit_properties( + self, qubit: Union[int, List[int]] + ) -> Union[QubitProperties, List[QubitProperties]]: + """Return QubitProperties for a given qubit. + + If there are no defined or the backend doesn't support querying these + details this method does not need to be implemented. + + Args: + qubit: The qubit to get the + :class:`~qiskit.provider.QubitProperties` object for. This can + be a single integer for 1 qubit or a list of qubits and a list + of :class:`~qiskit.provider.QubitProperties` objects will be + returned in the same order + + raises: + NotImplementedError: if the backend doesn't support querying the + qubit properties + """ + raise NotImplementedError + + def drive_channel(self, qubit: int): + """Return the drive channel for the given qubit. + + This is required to be implemented if the backend supports Pulse + scheduling. + + Returns: + DriveChannel: The Qubit drive channel + + Raises: + NotImplementedError: if the backend doesn't support querying the + measurement mapping + """ + raise NotImplementedError + + def measure_channel(self, qubit: int): + """Return the measure stimulus channel for the given qubit. + + This is required to be implemented if the backend supports Pulse + scheduling. + + Returns: + MeasureChannel: The Qubit measurement stimulus line + + Raises: + NotImplementedError: if the backend doesn't support querying the + measurement mapping + """ + raise NotImplementedError + + def acquire_channel(self, qubit: int): + """Return the acquisition channel for the given qubit. + + This is required to be implemented if the backend supports Pulse + scheduling. + + Returns: + AcquireChannel: The Qubit measurement acquisition line. + + Raises: + NotImplementedError: if the backend doesn't support querying the + measurement mapping + """ + raise NotImplementedError + + def control_channel(self, qubits: Iterable[int]): + """Return the secondary drive channel for the given qubit + + This is typically utilized for controlling multiqubit interactions. + This channel is derived from other channels. + + This is required to be implemented if the backend supports Pulse + scheduling. + + Args: + qubits: Tuple or list of qubits of the form + ``(control_qubit, target_qubit)``. + + Returns: + List[ControlChannel]: The Qubit measurement acquisition line. + + Raises: + NotImplementedError: if the backend doesn't support querying the + measurement mapping + """ + raise NotImplementedError + + def set_options(self, **fields): + """Set the options fields for the backend + + This method is used to update the options of a backend. If + you need to change any of the options prior to running just + pass in the kwarg with the new value for the options. + + Args: + fields: The fields to update the options + + Raises: + AttributeError: If the field passed in is not part of the + options + """ + for field in fields: + if not hasattr(self._options, field): + raise AttributeError("Options field %s is not valid for this backend" % field) + self._options.update_options(**fields) + + @property + def options(self): + """Return the options for the backend + + The options of a backend are the dynamic parameters defining + how the backend is used. These are used to control the :meth:`run` + method. + """ + return self._options + + @abstractmethod + def run(self, run_input, **options): + """Run on the backend. + + This method that will return a :class:`~qiskit.providers.Job` object + that run circuits. Depending on the backend this may be either an async + or sync call. It is the discretion of the provider to decide whether + running should block until the execution is finished or not. The Job + class can handle either situation. + + Args: + run_input (QuantumCircuit or Schedule or ScheduleBlock or list): An + individual or a list of + :class:`~qiskit.circuits.QuantumCircuit, + :class:`~qiskit.pulse.ScheduleBlock`, or + :class:`~qiskit.pulse.Schedule` objects to run on the backend. + options: Any kwarg options to pass to the backend for running the + config. If a key is also present in the options + attribute/object then the expectation is that the value + specified will be used instead of what's set in the options + object. + Returns: + Job: The job object for the run + """ + pass diff --git a/qiskit/providers/options.py b/qiskit/providers/options.py index b5d2469d16c4..0f8f1f66e9e4 100644 --- a/qiskit/providers/options.py +++ b/qiskit/providers/options.py @@ -12,10 +12,10 @@ """Container class for backend options.""" -import types +import io -class Options(types.SimpleNamespace): +class Options: """Base options object This class is the abstract class that all backend options are based @@ -26,10 +26,123 @@ class Options(types.SimpleNamespace): options. """ + _fields = {} + + def __init__(self, **kwargs): + self._fields = {} + self._fields.update(kwargs) + self.validator = {} + + def __repr__(self): + items = (f"{k}={v!r}" for k, v in self._fields.items()) + return "{}({})".format(type(self).__name__, ", ".join(items)) + + def __eq__(self, other): + if isinstance(self, Options) and isinstance(other, Options): + return self._fields == other._fields + return NotImplemented + + def set_validator(self, field, validator_value): + """Set an optional validator for a field in the options + + Setting a validator enables changes to an options values to be + validated for correctness when :meth:`~qiskit.providers.Options.update_options` + is called. For example if you have a numeric field like + ``shots`` you can specify a bounds tuple that set an upper and lower + bound on the value such as:: + + options.set_validator("shots", (1, 4096)) + + In this case whenever the ``"shots"`` option is updated by the user + it will enforce that the value is >=1 and <=4096. A ``ValueError`` will + be raised if it's outside those bounds. If a validator is already present + for the specified field it will be silently overriden. + + Args: + field (str): The field name to set the validator on + validator_value (list or tuple or type): The value to use for the + validator depending on the type indicates on how the value for + a field is enforced. If a tuple is passed in it must have a + length of two and will enforce the min and max value + (inclusive) for an integer or float value option. If it's a + list it will list the valid values for a field. If it's a + ``type`` the validator will just enforce the value is of a + certain type. + Raises: + KeyError: If field is not present in the options object + ValueError: If the ``validator_value`` has an invalid value for a + given type + TypeError: If ``validator_value`` is not a valid type + """ + + if field not in self._fields: + raise KeyError("Field '%s' is not present in this options object" % field) + if isinstance(validator_value, tuple): + if len(validator_value) != 2: + raise ValueError( + "A tuple validator must be of the form '(lower, upper)' " + "where lower and upper are the lower and upper bounds " + "inclusive of the numeric value" + ) + elif isinstance(validator_value, list): + if len(validator_value) == 0: + raise ValueError("A list validator must have at least one entry") + elif isinstance(validator_value, type): + pass + else: + raise TypeError( + f"{type(validator_value)} is not a valid validator type, it " + "must be a tuple, list, or class/type" + ) + self.validator[field] = validator_value + def update_options(self, **fields): """Update options with kwargs""" - self.__dict__.update(fields) + for field in fields: + field_validator = self.validator.get(field, None) + if isinstance(field_validator, tuple): + if fields[field] > field_validator[1] or fields[field] < field_validator[0]: + raise ValueError( + f"Specified value for '{field}' is not a valid value, " + f"must be >={field_validator[0]} or <={field_validator[1]}" + ) + elif isinstance(field_validator, list): + if fields[field] not in field_validator: + raise ValueError( + f"Specified value for {field} is not a valid choice, " + f"must be one of {field_validator}" + ) + elif isinstance(field_validator, type): + if not isinstance(fields[field], field_validator): + raise TypeError( + f"Specified value for {field} is not of required type {field_validator}" + ) + + self._fields.update(fields) + + def __getattr__(self, name): + try: + return self._fields[name] + except KeyError as ex: + raise AttributeError(f"Attribute {name} is not defined") from ex def get(self, field, default=None): """Get an option value for a given key.""" return getattr(self, field, default) + + def __str__(self): + no_validator = super().__str__() + if not self.validator: + return no_validator + else: + out_str = io.StringIO() + out_str.write(no_validator) + out_str.write("\nWhere:\n") + for field, value in self.validator.items(): + if isinstance(value, tuple): + out_str.write(f"\t{field} is >= {value[0]} and <= {value[1]}\n") + elif isinstance(value, list): + out_str.write(f"\t{field} is one of {value}\n") + elif isinstance(value, type): + out_str.write(f"\t{field} is of type {value}\n") + return out_str.getvalue() diff --git a/qiskit/test/mock/__init__.py b/qiskit/test/mock/__init__.py index 8afb6837392e..d578d50ff7df 100644 --- a/qiskit/test/mock/__init__.py +++ b/qiskit/test/mock/__init__.py @@ -23,6 +23,7 @@ from .fake_provider import FakeProvider, FakeLegacyProvider from .fake_provider import FakeProviderFactory from .fake_backend import FakeBackend, FakeLegacyBackend +from .fake_backend_v2 import FakeBackendV2 from .fake_job import FakeJob, FakeLegacyJob from .fake_qobj import FakeQobj diff --git a/qiskit/test/mock/fake_backend_v2.py b/qiskit/test/mock/fake_backend_v2.py new file mode 100644 index 000000000000..c4901191ab4b --- /dev/null +++ b/qiskit/test/mock/fake_backend_v2.py @@ -0,0 +1,97 @@ +# 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. + +# pylint: disable=no-name-in-module,import-error + +"""Mock BackendV2 object without run implemented for testing backwards compat""" + +import datetime + +import numpy as np + +from qiskit.circuit.parameter import Parameter +from qiskit.circuit.measure import Measure +from qiskit.circuit.library.standard_gates import CXGate, UGate, ECRGate, RXGate +from qiskit.providers.backend import BackendV2, QubitProperties +from qiskit.providers.options import Options +from qiskit.transpiler import Target, InstructionProperties + + +class FakeBackendV2(BackendV2): + """A mock backend that doesn't implement run() to test compatibility with Terra internals.""" + + def __init__(self): + super().__init__( + None, + name="FakeV2", + description="A fake BackendV2 example", + online_date=datetime.datetime.utcnow(), + backend_version="0.0.1", + ) + self._target = Target() + self._theta = Parameter("theta") + self._phi = Parameter("phi") + self._lam = Parameter("lambda") + rx_props = { + (0,): InstructionProperties(duration=5.23e-8, error=0.00038115), + (1,): InstructionProperties(duration=4.52e-8, error=0.00032115), + } + self._target.add_instruction(RXGate(self._theta), rx_props) + rx_30_props = { + (0,): InstructionProperties(duration=1.23e-8, error=0.00018115), + (1,): InstructionProperties(duration=1.52e-8, error=0.00012115), + } + self._target.add_instruction(RXGate(np.pi / 6), rx_30_props, name="rx_30") + u_props = { + (0,): InstructionProperties(duration=5.23e-8, error=0.00038115), + (1,): InstructionProperties(duration=4.52e-8, error=0.00032115), + } + self._target.add_instruction(UGate(self._theta, self._phi, self._lam), u_props) + cx_props = { + (0, 1): InstructionProperties(duration=5.23e-7, error=0.00098115), + (1, 0): InstructionProperties(duration=4.52e-7, error=0.00132115), + } + self._target.add_instruction(CXGate(), cx_props) + measure_props = { + (0,): InstructionProperties(duration=6e-6, error=5e-6), + (1,): InstructionProperties(duration=1e-6, error=9e-6), + } + self._target.add_instruction(Measure(), measure_props) + ecr_props = { + (1, 0): InstructionProperties(duration=4.52e-9, error=0.0000132115), + } + self._target.add_instruction(ECRGate(), ecr_props) + self.options.set_validator("shots", (1, 4096)) + self._qubit_properties = { + 0: QubitProperties(t1=63.48783e-6, t2=112.23246e-6, frequency=5.17538e9), + 1: QubitProperties(t1=73.09352e-6, t2=126.83382e-6, frequency=5.26722e9), + } + + @property + def target(self): + return self._target + + @property + def max_circuits(self): + return None + + @classmethod + def _default_options(cls): + return Options(shots=1024) + + def run(self, run_input, **options): + raise NotImplementedError + + def qubit_properties(self, qubit): + if isinstance(qubit, int): + return self._qubit_properties[qubit] + return [self._qubit_properties[i] for i in qubit] diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py index 14365ebaec62..2ad8cf09f9a4 100644 --- a/qiskit/transpiler/__init__.py +++ b/qiskit/transpiler/__init__.py @@ -358,6 +358,15 @@ Transpiler API ============== +Transpiler Target +----------------- + +.. autosummary:: + :toctree: ../stubs/ + + Target + InstructionProperties + Pass Manager Construction ------------------------- @@ -424,3 +433,5 @@ from .coupling import CouplingMap from .layout import Layout from .instruction_durations import InstructionDurations +from .target import Target +from .target import InstructionProperties diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py new file mode 100644 index 000000000000..dae7fb768d09 --- /dev/null +++ b/qiskit/transpiler/target.py @@ -0,0 +1,658 @@ +# 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. + +""" +A target object represents the minimum set of information the transpiler needs +from a backend +""" + +from collections.abc import Mapping +from collections import defaultdict +import io +import logging + +import retworkx as rx + +from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap +from qiskit.transpiler.coupling import CouplingMap +from qiskit.transpiler.instruction_durations import InstructionDurations +from qiskit.transpiler.timing_constraints import TimingConstraints + +logger = logging.getLogger(__name__) + + +class InstructionProperties: + """A representation of the properties of a gate implementation. + + This class provides the optional properties that a backend can provide + about an instruction. These represent the set that the transpiler can + currently work with if present. However, if your backend provides additional + properties for instructions you should subclass this to add additional + custom attributes for those custom/additional properties by the backend. + """ + + __slots__ = ("duration", "error", "calibration") + + def __init__( + self, + duration: float = None, + error: float = None, + calibration=None, + ): + """Create a new ``InstructionProperties`` object + + Args: + duration: The duration, in seconds, of the instruction on the + specified set of qubits + error: The average error rate for the instruction on the specified + set of qubits. + calibration (Union["qiskit.pulse.Schedule", "qiskit.pulse.ScheduleBlock"]): The pulse + representation of the instruction + """ + self.duration = duration + self.error = error + self.calibration = calibration + + def __repr__(self): + return ( + f"InstructionProperties(duration={self.duration}, error={self.error}" + f", calibration={self.calibration})" + ) + + +class Target(Mapping): + """ + The intent of the ``Target`` object is to inform Qiskit's compiler about + the constraints of a particular backend so the compiler can compile an + input circuit to something that works and is optimized for a device. It + currently contains a description of instructions on a backend and their + properties as well as some timing information. However, this exact + interface may evolve over time as the needs of the compiler change. These + changes will be done in a backwards compatible and controlled manner when + they are made (either through versioning, subclassing, or mixins) to add + on to the set of information exposed by a target. + + As a basic example, let's assume backend has two qubits, supports + :class:`~qiskit.circuit.library.UGate` on both qubits and + :class:`~qiskit.circuit.library.CXGate` in both directions. To model this + you would create the target like:: + + from qiskit.transpiler import Target, InstructionProperties + from qiskit.circuit.library import UGate, CXGate + from qiskit.circuit import Parameter + + gmap = Target() + theta = Parameter('theta') + phi = Parameter('phi') + lam = Parameter('lambda') + u_props = { + (0,): InstructionProperties(duration=5.23e-8, error=0.00038115), + (1,): InstructionProperties(duration=4.52e-8, error=0.00032115), + } + gmap.add_instruction(UGate(theta, phi, lam), u_props) + cx_props = { + (0,1): InstructionProperties(duration=5.23e-7, error=0.00098115), + (1,0): InstructionProperties(duration=4.52e-7, error=0.00132115), + } + gmap.add_instruction(CXGate(), cx_props) + + Each instruction in the Target is indexed by a unique string name that uniquely + identifies that instance of an :class:`~qiskit.circuit.Instruction` object in + the Target. There is a 1:1 mapping between a name and an + :class:`~qiskit.circuit.Instruction` instance in the target and each name must + be unique. By default the name is the :attr:`~qiskit.circuit.Instruction.name` + attribute of the instruction, but can be set to anything. This lets a single + target have multiple instances of the same instruction class with different + parameters. For example, if a backend target has two instances of an + :class:`~qiskit.circuit.library.RXGate` one is parameterized over any theta + while the other is tuned up for a theta of pi/6 you can add these by doing something + like:: + + import math + + from qiskit.transpiler import Target, InstructionProperties + from qiskit.circuit.library import RXGate + from qiskit.circuit import Parameter + + target = Target() + theta = Parameter('theta') + rx_props = { + (0,): InstructionProperties(duration=5.23e-8, error=0.00038115), + } + target.add_instruction(RXGate(theta), rx_props) + rx_30_props = { + (0,): InstructionProperties(duration=1.74e-6, error=.00012) + } + target.add_instruction(RXGate(math.pi / 6), rx_30_props, name='rx_30') + + Then in the ``target`` object accessing by ``rx_30`` will get the fixed + angle :class:`~qiskit.circuit.library.RXGate` while ``rx`` will get the + parameterized :class:`~qiskit.circuit.library.RXGate`. + + .. note:: + + This class assumes that qubit indices start at 0 and are a contiguous + set if you want a submapping the bits will need to be reindexed in + a new``Target`` object. + + .. note:: + + This class only supports additions of gates, qargs, and qubits. + If you need to remove one of these the best option is to iterate over + an existing object and create a new subset (or use one of the methods + to do this). The object internally caches different views and these + would potentially be invalidated by removals. + """ + + __slots__ = ( + "num_qubits", + "_gate_map", + "_gate_name_map", + "_qarg_gate_map", + "description", + "_coupling_graph", + "_instruction_durations", + "_instruction_schedule_map", + "dt", + "granularity", + "min_length", + "pulse_alignment", + "aquire_alignment", + ) + + def __init__( + self, + description=None, + num_qubits=0, + dt=None, + granularity=1, + min_length=1, + pulse_alignment=1, + aquire_alignment=1, + ): + """ + Create a new Target object + + Args: + description (str): An optional string to describe the Target. + num_qubits (int): An optional int to specify the number of qubits + the backend target has. If not set it will be implicitly set + based on the qargs when :meth:`~qiskit.Target.add_instruction` + is called. Note this must be set if the backend target is for a + noiseless simulator that doesn't have constraints on the + instructions so the transpiler knows how many qubits are + available. + dt (float): The system time resolution of input signals in seconds + granularity (int): An integer value representing minimum pulse gate + resolution in units of ``dt``. A user-defined pulse gate should + have duration of a multiple of this granularity value. + min_length (int): An integer value representing minimum pulse gate + length in units of ``dt``. A user-defined pulse gate should be + longer than this length. + pulse_alignment (int): An integer value representing a time + resolution of gate instruction starting time. Gate instruction + should start at time which is a multiple of the alignment + value. + acquire_alignment (int): An integer value representing a time + resolution of measure instruction starting time. Measure + instruction should start at time which is a multiple of the + alignment value. + """ + self.num_qubits = num_qubits + # A mapping of gate name -> gate instance + self._gate_name_map = {} + # A nested mapping of gate name -> qargs -> properties + self._gate_map = {} + # A mapping of qarg -> set(gate name) + self._qarg_gate_map = defaultdict(set) + self.dt = dt + self.description = description + self._coupling_graph = None + self._instruction_durations = None + self._instruction_schedule_map = None + self.granularity = granularity + self.min_length = min_length + self.pulse_alignment = pulse_alignment + self.aquire_alignment = aquire_alignment + + def add_instruction(self, instruction, properties=None, name=None): + """Add a new instruction to the :class:`~qiskit.transpiler.Target` + + As ``Target`` objects are strictly additive this is the primary method + for modifying a ``Target``. Typically you will use this to fully populate + a ``Target`` before using it in :class:`~qiskit.providers.BackendV2`. For + example:: + + from qiskit.circuit.library import CXGate + from qiskit.transpiler import Target, InstructionProperties + + target = Target() + cx_properties = { + (0, 1): None, + (1, 0): None, + (0, 2): None, + (2, 0): None, + (0, 3): None, + (2, 3): None, + (3, 0): None, + (3, 2): None + } + target.add_instruction(CXGate(), cx_properties) + + Will add a :class:`~qiskit.circuit.library.CXGate` to the target with no + properties (duration, error, etc) with the coupling edge list: + ``(0, 1), (1, 0), (0, 2), (2, 0), (0, 3), (2, 3), (3, 0), (3, 2)``. If + there are properties available for the instruction you can replace the + ``None`` value in the properties dictionary with an + :class:`~qiskit.transpiler.InstructionProperties` object. This pattern + is repeated for each :class:`~qiskit.circuit.Instruction` the target + supports. + + Args: + instruction (qiskit.circuit.Instruction): The operation object to add to the map. If it's + paramerterized any value of the parameter can be set + properties (dict): A dictionary of qarg entries to an + :class:`~qiskit.transpiler.InstructionProperties` object for that + instruction implementation on the backend. Properties are optional + for any instruction implementation, if there are no + :class:`~qiskit.transpiler.InstructionProperties` available for the + backend the value can be None. If there are no constraints on the + instruction (as in a noisless/ideal simulation) this can be set to + ``{None, None}`` which will indicate it runs on all qubits (or all + available permutations of qubits for multi-qubit gates). The first + ``None`` indicates it applies to all qubits and the second ``None`` + indicates there are no + :class:`~qiskit.transpiler.InstructionProperties` for the + instruction. By default, if properties is not set it is equivalent to + passing ``{None: None}``. + name (str): An optional name to use for identifying the instruction. If not + specified the :attr:`~qiskit.circuit.Instruction.name` attribute + of ``gate`` will be used. All gates in the ``Target`` need unique + names. Backends can differentiate between different + parameterizations of a single gate by providing a unique name for + each (e.g. `"rx30"`, `"rx60", ``"rx90"`` similar to the example in the + documentation for the :class:`~qiskit.transpiler.Target` class). + Raises: + AttributeError: If gate is already in map + """ + if properties is None: + properties = {None: None} + instruction_name = name or instruction.name + if instruction_name in self._gate_map: + raise AttributeError("Instruction %s is already in the target" % instruction_name) + self._gate_name_map[instruction_name] = instruction + qargs_val = {} + for qarg in properties: + if qarg is not None: + self.num_qubits = max(self.num_qubits, max(qarg) + 1) + qargs_val[qarg] = properties[qarg] + self._qarg_gate_map[qarg].add(instruction_name) + self._gate_map[instruction_name] = qargs_val + self._coupling_graph = None + self._instruction_durations = None + self._instruction_schedule_map = None + + def update_instruction_properties(self, instruction, qargs, properties): + """Update the property object for an instruction qarg pair already in the Target + + Args: + instruction (str): The instruction name to update + qargs (tuple): The qargs to update the properties of + properties (InstructionProperties): The properties to set for this nstruction + Raises: + KeyError: If ``instruction`` or ``qarg`` are not in the target + """ + if instruction not in self._gate_map: + raise KeyError(f"Provided instruction: '{instruction}' not in this Target") + if qargs not in self._gate_map[instruction]: + raise KeyError(f"Provided qarg: '{qargs}' not in this Target for {instruction}") + self._gate_map[instruction][qargs] = properties + self._instruction_durations = None + self._instruction_schedule_map = None + + def update_from_instruction_schedule_map(self, inst_map, inst_name_map=None, error_dict=None): + """Update the target from an instruction schedule map. + + If the input instruction schedule map contains new instructions not in + the target they will be added. However if it contains additional qargs + for an existing instruction in the target it will error. + + Args: + inst_map (InstructionScheduleMap): The instruction + inst_name_map (dict): An optional dictionary that maps any + instruction name in ``inst_map`` to an instruction object + error_dict (dict): A dictionary of errors of the form:: + + {gate_name: {qarg: error}} + + for example:: + + {'rx': {(0, ): 1.4e-4, (1, ): 1.2e-4}} + + For each entry in the ``inst_map`` if ``error_dict`` is defined + a when updating the ``Target`` the error value will be pulled from + this dictionary. If one is not found in ``error_dict`` then + ``None`` will be used. + + Raises: + ValueError: If ``inst_map`` contains new instructions and + ``inst_name_map`` isn't specified + KeyError: If a ``inst_map`` contains a qarg for an instruction + that's not in the target + """ + for inst in inst_map.instructions: + out_props = {} + for qarg in inst_map.qubits_with_instruction(inst): + sched = inst_map.get(inst, qarg) + val = InstructionProperties(calibration=sched) + try: + qarg = tuple(qarg) + except TypeError: + qarg = (qarg,) + if inst in self._gate_map: + if self.dt is not None: + val.duration = sched.duration * self.dt + else: + val.duration = None + if error_dict is not None: + error_inst = error_dict.get(inst) + if error_inst: + error = error_inst.get(qarg) + val.error = error + else: + val.error = None + else: + val.error = None + out_props[qarg] = val + if inst not in self._gate_map: + if inst_name_map is not None: + self.add_instruction(inst_name_map[inst], out_props, name=inst) + else: + raise ValueError( + "An inst_name_map kwarg must be specified to add new " + "instructions from an InstructionScheduleMap" + ) + else: + for qarg, prop in out_props.items(): + self.update_instruction_properties(inst, qarg, prop) + + @property + def qargs(self): + """The set of qargs in the target.""" + if None in self._qarg_gate_map: + return None + return self._qarg_gate_map.keys() + + def qargs_for_operation_name(self, operation): + """Get the qargs for a given operation name + + Args: + operation (str): The operation name to get qargs for + Returns: + set: The set of qargs the gate instance applies to. + """ + if None in self._gate_map[operation]: + return None + return self._gate_map[operation].keys() + + def durations(self): + """Get an InstructionDurations object from the target + + Returns: + InstructionDurations: The instruction duration represented in the + target + """ + if self._instruction_durations is not None: + return self._instruction_durations + out_durations = [] + for instruction, props_map in self._gate_map.items(): + for qarg, properties in props_map.items(): + if properties is not None and properties.duration is not None: + out_durations.append((instruction, list(qarg), properties.duration, "s")) + self._instruction_durations = InstructionDurations(out_durations, dt=self.dt) + return self._instruction_durations + + def timing_constraints(self): + """Get an :class:`~qiskit.transpiler.TimingConstraints` object from the target + + Returns: + TimingConstraints: The timing constraints represented in the Target + """ + return TimingConstraints( + self.granularity, self.min_length, self.pulse_alignment, self.aquire_alignment + ) + + def instruction_schedule_map(self): + """Return an :class:`~qiskit.pulse.InstructionScheduleMap` for the + instructions in the target with a pulse schedule defined. + + Returns: + InstructionScheduleMap: The instruction schedule map for the + instructions in this target with a pulse schedule defined. + """ + if self._instruction_schedule_map is not None: + return self._instruction_schedule_map + out_inst_schedule_map = InstructionScheduleMap() + for instruction, qargs in self._gate_map.items(): + for qarg, properties in qargs.items(): + if properties is not None and properties.calibration is not None: + out_inst_schedule_map.add(instruction, qarg, properties.calibration) + self._instruction_schedule_map = out_inst_schedule_map + return out_inst_schedule_map + + def operation_from_name(self, instruction): + """Get the operation class object for a given name + + Args: + instruction (str): The instruction name to get the + :class:`~qiskit.circuit.Instruction` instance for + Returns: + qiskit.circuit.Instruction: The Instruction instance corresponding to the name + """ + return self._gate_name_map[instruction] + + def operations_for_qargs(self, qargs): + """Get the operation class object for a specified qarg + + Args: + qargs (tuple): A qargs tuple of the qubits to get the gates that apply + to it. For example, ``(0,)`` will return the set of all + instructions that apply to qubit 0. + Returns: + list: The list of :class:`~qiskit.circuit.Instruction` instances + that apply to the specified qarg. + + Raises: + KeyError: If qargs is not in target + """ + if qargs not in self._qarg_gate_map: + raise KeyError(f"{qargs} not in target.") + return [self._gate_name_map[x] for x in self._qarg_gate_map[qargs]] + + @property + def operation_names(self): + """Get the operation names in the target.""" + return self._gate_map.keys() + + @property + def operations(self): + """Get the operation class objects in the target.""" + return list(self._gate_name_map.values()) + + @property + def instructions(self): + """Get the list of tuples ``(:class:`~qiskit.circuit.Instruction`, (qargs))`` + for the target""" + return [ + (self._gate_name_map[op], qarg) for op in self._gate_map for qarg in self._gate_map[op] + ] + + def instruction_properties(self, index): + """Get the instruction properties for a specific instruction tuple + + This method is to be used in conjunction with the + :attr:`~qiskit.transpiler.Target.instructions` attribute of a + :class:`~qiskit.transpiler.Target` object. You can use this method to quickly + get the instruction properties for an element of + :attr:`~qiskit.transpiler.Target.instructions` by using the index in that list. + However, if you're not working with :attr:`~qiskit.transpiler.Target.instructions` + directly it is likely more efficient to access the target directly via the name + and qubits to get the instruction properties. For example, if + :attr:`~qiskit.transpiler.Target.instructions` returned:: + + [(XGate(), (0,)), (XGate(), (1,))] + + you could get the properties of the ``XGate`` on qubit 1 with:: + + props = target.instruction_properties(1) + + but just accessing it directly via the name would be more efficient:: + + props = target['x'][(1,)] + + (assuming the ``XGate``'s canonical name in the target is ``'x'``) + This is especially true for larger targets as this will scale worse with the number + of instruction tuples in a target. + + Args: + index (int): The index of the instruction tuple from the + :attr:`~qiskit.transpiler.Target.instructions` attribute. For, example + if you want the properties from the third element in + :attr:`~qiskit.transpiler.Target.instructions` you would set this to be ``2``. + Returns: + InstructionProperties: The instruction properties for the specified instruction tuple + """ + instruction_properties = [ + inst_props for op in self._gate_map for _, inst_props in self._gate_map[op].items() + ] + return instruction_properties[index] + + def _build_coupling_graph(self): + self._coupling_graph = rx.PyDiGraph(multigraph=False) + self._coupling_graph.add_nodes_from([{} for _ in range(self.num_qubits)]) + for gate, qarg_map in self._gate_map.items(): + for qarg, properties in qarg_map.items(): + if len(qarg) == 1: + self._coupling_graph[qarg[0]] = properties + elif len(qarg) == 2: + try: + edge_data = self._coupling_graph.get_edge_data(*qarg) + edge_data[gate] = properties + except rx.NoEdgeBetweenNodes: + self._coupling_graph.add_edge(*qarg, {gate: properties}) + + def build_coupling_map(self, two_q_gate=None): + """Get a :class:`~qiskit.transpiler.CouplingMap` from this target. + + Args: + two_q_gate (str): An optional gate name for a two qubit gate in + the Target to generate the coupling map for. If specified the + output coupling map will only have edges between qubits where + this gate is present. + Returns: + CouplingMap: The :class:`~qiskit.transpiler.CouplingMap` object + for this target. + + Raises: + ValueError: If a non-two qubit gate is passed in for ``two_q_gate``. + IndexError: If an Instruction not in the Target is passed in for + ``two_q_gate``. + """ + if None in self._qarg_gate_map: + return None + if any(len(x) > 2 for x in self.qargs): + logger.warning( + "This Target object contains multiqubit gates that " + "operate on > 2 qubits. This will not be reflected in " + "the output coupling map." + ) + + if two_q_gate is not None: + coupling_graph = rx.PyDiGraph(multigraph=False) + coupling_graph.add_nodes_from(list(None for _ in range(self.num_qubits))) + for qargs, properties in self._gate_map[two_q_gate].items(): + if len(qargs) != 2: + raise ValueError( + "Specified two_q_gate: %s is not a 2 qubit instruction" % two_q_gate + ) + coupling_graph.add_edge(*qargs, {two_q_gate: properties}) + cmap = CouplingMap() + cmap.graph = coupling_graph + return cmap + + if self._coupling_graph is None: + self._build_coupling_graph() + cmap = CouplingMap() + cmap.graph = self._coupling_graph + return cmap + + @property + def physical_qubits(self): + """Returns a sorted list of physical_qubits""" + return list(range(self.num_qubits)) + + def __iter__(self): + return iter(self._gate_map) + + def __getitem__(self, key): + return self._gate_map[key] + + def __len__(self): + return len(self._gate_map) + + def __contains__(self, item): + return item in self._gate_map + + def keys(self): + return self._gate_map.keys() + + def values(self): + return self._gate_map.values() + + def items(self): + return self._gate_map.items() + + def __str__(self): + output = io.StringIO() + if self.description is not None: + output.write(f"Target: {self.description}\n") + else: + output.write("Target\n") + output.write(f"Number of qubits: {self.num_qubits}\n") + output.write("Instructions:\n") + for inst, qarg_props in self._gate_map.items(): + output.write(f"\t{inst}\n") + for qarg, props in qarg_props.items(): + if qarg is None: + continue + if props is None: + output.write(f"\t\t{qarg}\n") + continue + prop_str_pieces = [f"\t\t{qarg}:\n"] + duration = getattr(props, "duration", None) + if duration is not None: + prop_str_pieces.append(f"\t\t\tDuration: {duration} sec.\n") + error = getattr(props, "error", None) + if error is not None: + prop_str_pieces.append(f"\t\t\tError Rate: {error}\n") + schedule = getattr(props, "calibration", None) + if schedule is not None: + prop_str_pieces.append("\t\t\tWith pulse schedule calibration\n") + extra_props = getattr(props, "properties", None) + if extra_props is not None: + extra_props_pieces = [ + f"\t\t\t\t{key}: {value}\n" for key, value in extra_props.items() + ] + extra_props_str = "".join(extra_props_pieces) + prop_str_pieces.append(f"\t\t\tExtra properties:\n{extra_props_str}\n") + output.write("".join(prop_str_pieces)) + return output.getvalue() diff --git a/releasenotes/notes/add-backend-v2-ce84c976fb13b038.yaml b/releasenotes/notes/add-backend-v2-ce84c976fb13b038.yaml new file mode 100644 index 000000000000..3138c7beed53 --- /dev/null +++ b/releasenotes/notes/add-backend-v2-ce84c976fb13b038.yaml @@ -0,0 +1,87 @@ +--- +features: + - | + Added a new version of the :class:`~qiskit.providers.Backend` interface, + :class:`~qiskit.providers.BackendV2`. This new version is a large change + from the previous version, :class:`~qiskit.providers.BackendV1` and + changes both the user access pattern for properties of the backend (like + number of qubits, etc) and how the backend represents its constraints + to the transpiler. The execution of circuits (via the + :meth:`~qiskit.providers.BackendV2.run` method) remains unchanged. With + a :class:`~qiskit.providers.BackendV2` backend instead of having a separate + :meth:`~qiskit.providers.BackendV1.configuration`, + :meth:`~qiskit.providers.BackendV1.properties`, and + :meth:`~qiskit.providers.BackendV1.defaults` methods that construct + :class:`~qiskit.providers.models.BackendConfiguration`, + :class:`~qiskit.providers.models.BackendProperties`, and + :class:`~qiskit.providers.models.PulseDefaults` objects respectively, + like in the :class:`~qiskit.providers.BackendV1` interface, the attributes + contained in those output objects are accessible directly as attributes of + the :class:`~qiskit.providers.BackendV2` object. For example, to get the + number of qubits for a backend with :class:`~qiskit.providers.BackendV1` + you would do:: + + num_qubits = backend.configuration().n_qubits + + while with :class:`~qiskit.providers.BackendV2` it is:: + + num_qubits = backend.num_qubits + + The other change around this is that the number of attributes exposed in + the abstract :class:`~qiskit.providers.BackendV2` class is designed to be + a hardware/vendor agnostic set of the required or optional fields that the + rest of Qiskit can use today with any backend. Sub-classes of the abstract + :class:`~qiskit.providers.BackendV2` class can add support for additional + attributes and methods beyond those defined in + :class:`~qiskit.providers.BackendV2`, but these will not be supported + universally throughout Qiskit. + + The other critical change that is primarily important for provider authors is + how a :class:`~qiskit.providers.BackendV2` exposes the properties of + a particular backend to the transpiler. With + :class:`~qiskit.providers.BackendV2` this is done via a + :class:`~qiskit.transpiler.Target` object. The + :class:`~qiskit.transpiler.Target`, which is exposed via the + :attr:`~qiskit.providers.BackendV2.target` attribute, is used to represent + the set of constraints for running circuits on a particular backend. It + contains the subset of information previously exposed by the + :class:`~qiskit.providers.models.BackendConfiguration`, + :class:`~qiskit.providers.models.BackendProperties`, and + :class:`~qiskit.providers.models.PulseDefaults` classes which the transpiler + can actively use. When migrating a provider to use + :class:`~qiskit.providers.BackendV2` (or when creating a new provider + package) the construction of backend objects will primarily be around + creating a :class:`~qiskit.transpiler.Target` object for the backend. + - | + Added a new :class:`~qiskit.transpiler.Target` class to the + :mod:`~qiskit.transpiler` module. The :class:`~qiskit.transpiler.Target` + class is designed to represent the constraints of backend to the compiler. + The :class:`~qiskit.transpiler.Target` class is intended to be used + with a :class:`~qiskit.providers.BackendV2` backend and is how backends + will model their constraints for the transpiler moving forward. It combines + the previously distinct fields used for controlling the + :func:`~qiskit.compiler.transpile` target device (e.g. ``basis_gates``, + ``coupling_map``, ``instruction_durations``, etc) into a single data + structure. It also adds additional functionality on top of what was + available previously such as representing heterogeneous gate sets, + multi-qubit gate connectivity, and tuned variants of the same gates. + Currently the transpiler doesn't factor in all these constraints, but + over time it will grow to leverage the extra functionality. + - | + The :class:`~qiskit.providers.Options` class now has optional support for + specifying validators. This enables :class:`~qiskit.providers.Backend` + authors to optionally specify basic validation on the user supplied values + for fields in the :class:`~qiskit.providers.Options` object. For example, + if you had an :class:`~qiskit.providers.Options` object defined with:: + + from qiskit.providers.Options + options = Options(shots=1024) + + you can set a validator on shots for it to be between 1 and 4096 with:: + + options.set_validator('shots', (1, 4096)) + + With the validator set any call to the + :meth:`~qiskit.providers.Options.update_options` method will check that + if ``shots`` is being updated the proposed new value is within the valid + range. diff --git a/test/python/providers/test_backend_v2.py b/test/python/providers/test_backend_v2.py new file mode 100644 index 000000000000..a81bbef3d94b --- /dev/null +++ b/test/python/providers/test_backend_v2.py @@ -0,0 +1,68 @@ +# 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. + +# pylint: disable=missing-class-docstring,missing-function-docstring +# pylint: disable=missing-module-docstring + +import math + +from qiskit.circuit import QuantumCircuit +from qiskit.compiler import transpile +from qiskit.test.base import QiskitTestCase +from qiskit.test.mock.fake_backend_v2 import FakeBackendV2 + + +class TestBackendV2(QiskitTestCase): + def setUp(self): + super().setUp() + self.backend = FakeBackendV2() + + def test_qubit_properties(self): + """Test that qubit properties are returned as expected.""" + props = self.backend.qubit_properties([1, 0]) + self.assertEqual([73.09352e-6, 63.48783e-6], [x.t1 for x in props]) + self.assertEqual([126.83382e-6, 112.23246e-6], [x.t2 for x in props]) + self.assertEqual([5.26722e9, 5.17538e9], [x.frequency for x in props]) + + def test_option_bounds(self): + """Test that option bounds are enforced.""" + with self.assertRaises(ValueError) as cm: + self.backend.set_options(shots=8192) + self.assertEqual( + str(cm.exception), + "Specified value for 'shots' is not a valid value, must be >=1 or <=4096", + ) + + def test_transpile(self): + """Test that transpile() works with a BackendV2 backend.""" + qc = QuantumCircuit(2) + qc.h(1) + qc.cz(1, 0) + qc.measure_all() + with self.assertLogs("qiskit.providers.backend", level="WARN") as log: + tqc = transpile(qc, self.backend) + self.assertEqual( + log.output, + [ + "WARNING:qiskit.providers.backend:This backend's operations: " + "ecr only apply to a subset of qubits. Using this property to " + "get 'basis_gates' for the transpiler may potentially create " + "invalid output" + ], + ) + expected = QuantumCircuit(2) + expected.u(math.pi / 2, 0, -math.pi, 0) + expected.u(math.pi / 2, 0, -math.pi, 1) + expected.cx(1, 0) + expected.u(math.pi / 2, 0, -math.pi, 0) + expected.measure_all() + self.assertEqual(tqc, expected) diff --git a/test/python/providers/test_options.py b/test/python/providers/test_options.py new file mode 100644 index 000000000000..64c61c7a7a8e --- /dev/null +++ b/test/python/providers/test_options.py @@ -0,0 +1,107 @@ +# 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. + +# pylint: disable=missing-class-docstring,missing-function-docstring +# pylint: disable=missing-module-docstring + +from qiskit.providers import Options +from qiskit.qobj.utils import MeasLevel + +from qiskit.test import QiskitTestCase + + +class TestOptions(QiskitTestCase): + def test_no_validators(self): + options = Options(shots=1024, method="auto", meas_level=MeasLevel.KERNELED) + self.assertEqual(options.shots, 1024) + options.update_options(method="statevector") + self.assertEqual(options.method, "statevector") + + def test_no_validators_str(self): + options = Options(shots=1024, method="auto", meas_level=MeasLevel.KERNELED) + self.assertEqual( + str(options), "Options(shots=1024, method='auto', meas_level=)" + ) + + def test_range_bound_validator(self): + options = Options(shots=1024) + options.set_validator("shots", (1, 4096)) + with self.assertRaises(ValueError): + options.update_options(shots=8192) + + def test_range_bound_string(self): + options = Options(shots=1024) + options.set_validator("shots", (1, 1024)) + expected = """Options(shots=1024) +Where: +\tshots is >= 1 and <= 1024\n""" + self.assertEqual(str(options), expected) + + def test_list_choice(self): + options = Options(method="auto") + options.set_validator("method", ["auto", "statevector", "mps"]) + with self.assertRaises(ValueError): + options.update_options(method="stabilizer") + options.update_options(method="mps") + self.assertEqual(options.method, "mps") + + def test_list_choice_string(self): + options = Options(method="auto") + options.set_validator("method", ["auto", "statevector", "mps"]) + expected = """Options(method='auto') +Where: +\tmethod is one of ['auto', 'statevector', 'mps']\n""" + self.assertEqual(str(options), expected) + + def test_type_validator(self): + options = Options(meas_level=MeasLevel.KERNELED) + options.set_validator("meas_level", MeasLevel) + with self.assertRaises(TypeError): + options.update_options(meas_level=2) + options.update_options(meas_level=MeasLevel.CLASSIFIED) + self.assertEqual(2, options.meas_level.value) + + def test_type_validator_str(self): + options = Options(meas_level=MeasLevel.KERNELED) + options.set_validator("meas_level", MeasLevel) + expected = """Options(meas_level=) +Where: +\tmeas_level is of type \n""" + self.assertEqual(str(options), expected) + + def test_range_bound_validator_multiple_fields(self): + options = Options(shots=1024, method="auto", meas_level=MeasLevel.KERNELED) + options.set_validator("shots", (1, 1024)) + options.set_validator("method", ["auto", "statevector", "mps"]) + options.set_validator("meas_level", MeasLevel) + with self.assertRaises(ValueError): + options.update_options(shots=2048, method="statevector") + options.update_options(shots=512, method="statevector") + self.assertEqual(options.shots, 512) + self.assertEqual(options.method, "statevector") + + def test_range_bound_validator_multiple_fields_string(self): + options = Options(shots=1024, method="auto", meas_level=MeasLevel.KERNELED) + options.set_validator("shots", (1, 1024)) + options.set_validator("method", ["auto", "statevector", "mps"]) + options.set_validator("meas_level", MeasLevel) + expected = """Options(shots=1024, method='auto', meas_level=) +Where: +\tshots is >= 1 and <= 1024 +\tmethod is one of ['auto', 'statevector', 'mps'] +\tmeas_level is of type \n""" + self.assertEqual(str(options), expected) + + def test_hasattr(self): + options = Options(shots=1024) + self.assertTrue(hasattr(options, "shots")) + self.assertFalse(hasattr(options, "method")) diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py new file mode 100644 index 000000000000..d518ec7fce58 --- /dev/null +++ b/test/python/transpiler/test_target.py @@ -0,0 +1,1042 @@ +# 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. + +# pylint: disable=missing-docstring + +import math + +from qiskit.circuit.library import ( + RZGate, + SXGate, + XGate, + CXGate, + RYGate, + RXGate, + RXXGate, + RGate, + IGate, + ECRGate, + UGate, + CCXGate, +) +from qiskit.circuit.measure import Measure +from qiskit.circuit.parameter import Parameter +from qiskit import pulse +from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap +from qiskit.transpiler.coupling import CouplingMap +from qiskit.transpiler.instruction_durations import InstructionDurations +from qiskit.transpiler.timing_constraints import TimingConstraints +from qiskit.transpiler import Target +from qiskit.transpiler import InstructionProperties +from qiskit.test import QiskitTestCase +from qiskit.test.mock.fake_backend_v2 import FakeBackendV2 + + +class TestTarget(QiskitTestCase): + def setUp(self): + super().setUp() + self.fake_backend = FakeBackendV2() + self.fake_backend_target = self.fake_backend.target + self.theta = Parameter("theta") + self.phi = Parameter("phi") + self.ibm_target = Target() + i_props = { + (0,): InstructionProperties(duration=35.5e-9, error=0.000413), + (1,): InstructionProperties(duration=35.5e-9, error=0.000502), + (2,): InstructionProperties(duration=35.5e-9, error=0.0004003), + (3,): InstructionProperties(duration=35.5e-9, error=0.000614), + (4,): InstructionProperties(duration=35.5e-9, error=0.006149), + } + self.ibm_target.add_instruction(IGate(), i_props) + rz_props = { + (0,): InstructionProperties(duration=0, error=0), + (1,): InstructionProperties(duration=0, error=0), + (2,): InstructionProperties(duration=0, error=0), + (3,): InstructionProperties(duration=0, error=0), + (4,): InstructionProperties(duration=0, error=0), + } + self.ibm_target.add_instruction(RZGate(self.theta), rz_props) + sx_props = { + (0,): InstructionProperties(duration=35.5e-9, error=0.000413), + (1,): InstructionProperties(duration=35.5e-9, error=0.000502), + (2,): InstructionProperties(duration=35.5e-9, error=0.0004003), + (3,): InstructionProperties(duration=35.5e-9, error=0.000614), + (4,): InstructionProperties(duration=35.5e-9, error=0.006149), + } + self.ibm_target.add_instruction(SXGate(), sx_props) + x_props = { + (0,): InstructionProperties(duration=35.5e-9, error=0.000413), + (1,): InstructionProperties(duration=35.5e-9, error=0.000502), + (2,): InstructionProperties(duration=35.5e-9, error=0.0004003), + (3,): InstructionProperties(duration=35.5e-9, error=0.000614), + (4,): InstructionProperties(duration=35.5e-9, error=0.006149), + } + self.ibm_target.add_instruction(XGate(), x_props) + cx_props = { + (3, 4): InstructionProperties(duration=270.22e-9, error=0.00713), + (4, 3): InstructionProperties(duration=305.77e-9, error=0.00713), + (3, 1): InstructionProperties(duration=462.22e-9, error=0.00929), + (1, 3): InstructionProperties(duration=497.77e-9, error=0.00929), + (1, 2): InstructionProperties(duration=227.55e-9, error=0.00659), + (2, 1): InstructionProperties(duration=263.11e-9, error=0.00659), + (0, 1): InstructionProperties(duration=519.11e-9, error=0.01201), + (1, 0): InstructionProperties(duration=554.66e-9, error=0.01201), + } + self.ibm_target.add_instruction(CXGate(), cx_props) + measure_props = { + (0,): InstructionProperties(duration=5.813e-6, error=0.0751), + (1,): InstructionProperties(duration=5.813e-6, error=0.0225), + (2,): InstructionProperties(duration=5.813e-6, error=0.0146), + (3,): InstructionProperties(duration=5.813e-6, error=0.0215), + (4,): InstructionProperties(duration=5.813e-6, error=0.0333), + } + self.ibm_target.add_instruction(Measure(), measure_props) + + self.aqt_target = Target(description="AQT Target") + rx_props = { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + } + self.aqt_target.add_instruction(RXGate(self.theta), rx_props) + ry_props = { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + } + self.aqt_target.add_instruction(RYGate(self.theta), ry_props) + rz_props = { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + } + self.aqt_target.add_instruction(RZGate(self.theta), rz_props) + r_props = { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + } + self.aqt_target.add_instruction(RGate(self.theta, self.phi), r_props) + rxx_props = { + (0, 1): None, + (0, 2): None, + (0, 3): None, + (0, 4): None, + (1, 0): None, + (2, 0): None, + (3, 0): None, + (4, 0): None, + (1, 2): None, + (1, 3): None, + (1, 4): None, + (2, 1): None, + (3, 1): None, + (4, 1): None, + (2, 3): None, + (2, 4): None, + (3, 2): None, + (4, 2): None, + (3, 4): None, + (4, 3): None, + } + self.aqt_target.add_instruction(RXXGate(self.theta), rxx_props) + measure_props = { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + } + self.aqt_target.add_instruction(Measure(), measure_props) + self.empty_target = Target() + self.ideal_sim_target = Target(num_qubits=3, description="Ideal Simulator") + self.lam = Parameter("lam") + for inst in [ + UGate(self.theta, self.phi, self.lam), + RXGate(self.theta), + RYGate(self.theta), + RZGate(self.theta), + CXGate(), + ECRGate(), + CCXGate(), + Measure(), + ]: + self.ideal_sim_target.add_instruction(inst, {None: None}) + + def test_qargs(self): + self.assertEqual(set(), self.empty_target.qargs) + expected_ibm = { + (0,), + (1,), + (2,), + (3,), + (4,), + (3, 4), + (4, 3), + (3, 1), + (1, 3), + (1, 2), + (2, 1), + (0, 1), + (1, 0), + } + self.assertEqual(expected_ibm, self.ibm_target.qargs) + expected_aqt = { + (0,), + (1,), + (2,), + (3,), + (4,), + (0, 1), + (0, 2), + (0, 3), + (0, 4), + (1, 0), + (2, 0), + (3, 0), + (4, 0), + (1, 2), + (1, 3), + (1, 4), + (2, 1), + (3, 1), + (4, 1), + (2, 3), + (2, 4), + (3, 2), + (4, 2), + (3, 4), + (4, 3), + } + self.assertEqual(expected_aqt, self.aqt_target.qargs) + expected_fake = { + (0,), + (1,), + (0, 1), + (1, 0), + } + self.assertEqual(expected_fake, self.fake_backend_target.qargs) + self.assertEqual(None, self.ideal_sim_target.qargs) + + def test_qargs_for_operation_name(self): + with self.assertRaises(KeyError): + self.empty_target.qargs_for_operation_name("rz") + self.assertEqual( + self.ibm_target.qargs_for_operation_name("rz"), {(0,), (1,), (2,), (3,), (4,)} + ) + self.assertEqual( + self.aqt_target.qargs_for_operation_name("rz"), {(0,), (1,), (2,), (3,), (4,)} + ) + self.assertEqual(self.fake_backend_target.qargs_for_operation_name("cx"), {(0, 1), (1, 0)}) + self.assertEqual( + self.fake_backend_target.qargs_for_operation_name("ecr"), + { + (1, 0), + }, + ) + self.assertEqual(self.ideal_sim_target.qargs_for_operation_name("cx"), None) + + def test_instruction_names(self): + self.assertEqual(self.empty_target.operation_names, set()) + self.assertEqual(self.ibm_target.operation_names, {"rz", "id", "sx", "x", "cx", "measure"}) + self.assertEqual(self.aqt_target.operation_names, {"rz", "ry", "rx", "rxx", "r", "measure"}) + self.assertEqual( + self.fake_backend_target.operation_names, {"u", "cx", "measure", "ecr", "rx_30", "rx"} + ) + self.assertEqual( + self.ideal_sim_target.operation_names, + {"u", "rz", "ry", "rx", "cx", "ecr", "ccx", "measure"}, + ) + + def test_operations(self): + self.assertEqual(self.empty_target.operations, []) + ibm_expected = [RZGate(self.theta), IGate(), SXGate(), XGate(), CXGate(), Measure()] + for gate in ibm_expected: + self.assertIn(gate, self.ibm_target.operations) + aqt_expected = [ + RZGate(self.theta), + RXGate(self.theta), + RYGate(self.theta), + RGate(self.theta, self.phi), + RXXGate(self.theta), + ] + for gate in aqt_expected: + self.assertIn(gate, self.aqt_target.operations) + fake_expected = [ + UGate(self.fake_backend._theta, self.fake_backend._phi, self.fake_backend._lam), + CXGate(), + Measure(), + ECRGate(), + RXGate(math.pi / 6), + RXGate(self.fake_backend._theta), + ] + for gate in fake_expected: + self.assertIn(gate, self.fake_backend_target.operations) + ideal_sim_expected = [ + UGate(self.theta, self.phi, self.lam), + RXGate(self.theta), + RYGate(self.theta), + RZGate(self.theta), + CXGate(), + ECRGate(), + CCXGate(), + Measure(), + ] + for gate in ideal_sim_expected: + self.assertIn(gate, self.ideal_sim_target.operations) + + def test_instructions(self): + self.assertEqual(self.empty_target.instructions, []) + ibm_expected = [ + (IGate(), (0,)), + (IGate(), (1,)), + (IGate(), (2,)), + (IGate(), (3,)), + (IGate(), (4,)), + (RZGate(self.theta), (0,)), + (RZGate(self.theta), (1,)), + (RZGate(self.theta), (2,)), + (RZGate(self.theta), (3,)), + (RZGate(self.theta), (4,)), + (SXGate(), (0,)), + (SXGate(), (1,)), + (SXGate(), (2,)), + (SXGate(), (3,)), + (SXGate(), (4,)), + (XGate(), (0,)), + (XGate(), (1,)), + (XGate(), (2,)), + (XGate(), (3,)), + (XGate(), (4,)), + (CXGate(), (3, 4)), + (CXGate(), (4, 3)), + (CXGate(), (3, 1)), + (CXGate(), (1, 3)), + (CXGate(), (1, 2)), + (CXGate(), (2, 1)), + (CXGate(), (0, 1)), + (CXGate(), (1, 0)), + (Measure(), (0,)), + (Measure(), (1,)), + (Measure(), (2,)), + (Measure(), (3,)), + (Measure(), (4,)), + ] + self.assertEqual(ibm_expected, self.ibm_target.instructions) + ideal_sim_expected = [ + (UGate(self.theta, self.phi, self.lam), None), + (RXGate(self.theta), None), + (RYGate(self.theta), None), + (RZGate(self.theta), None), + (CXGate(), None), + (ECRGate(), None), + (CCXGate(), None), + (Measure(), None), + ] + self.assertEqual(ideal_sim_expected, self.ideal_sim_target.instructions) + + def test_instruction_properties(self): + i_gate_2 = self.ibm_target.instruction_properties(2) + self.assertEqual(i_gate_2.error, 0.0004003) + self.assertIsNone(self.ideal_sim_target.instruction_properties(4)) + + def test_get_instruction_from_name(self): + with self.assertRaises(KeyError): + self.empty_target.operation_from_name("measure") + self.assertEqual(self.ibm_target.operation_from_name("measure"), Measure()) + self.assertEqual(self.fake_backend_target.operation_from_name("rx_30"), RXGate(math.pi / 6)) + self.assertEqual( + self.fake_backend_target.operation_from_name("rx"), + RXGate(self.fake_backend._theta), + ) + self.assertEqual(self.ideal_sim_target.operation_from_name("ccx"), CCXGate()) + + def test_get_instructions_for_qargs(self): + with self.assertRaises(KeyError): + self.empty_target.operations_for_qargs((0,)) + expected = [RZGate(self.theta), IGate(), SXGate(), XGate(), Measure()] + res = self.ibm_target.operations_for_qargs((0,)) + for gate in expected: + self.assertIn(gate, res) + expected = [CXGate(), ECRGate()] + res = self.fake_backend_target.operations_for_qargs((1, 0)) + for gate in expected: + self.assertIn(gate, res) + expected = [CXGate()] + res = self.fake_backend_target.operations_for_qargs((0, 1)) + self.assertEqual(expected, res) + ideal_sim_expected = [ + UGate(self.theta, self.phi, self.lam), + RXGate(self.theta), + RYGate(self.theta), + RZGate(self.theta), + CXGate(), + ECRGate(), + CCXGate(), + Measure(), + ] + for gate in ideal_sim_expected: + self.assertIn(gate, self.ideal_sim_target.operations_for_qargs(None)) + + def test_coupling_map(self): + self.assertEqual( + CouplingMap().get_edges(), self.empty_target.build_coupling_map().get_edges() + ) + self.assertEqual( + set(CouplingMap.from_full(5).get_edges()), + set(self.aqt_target.build_coupling_map().get_edges()), + ) + self.assertEqual( + {(0, 1), (1, 0)}, set(self.fake_backend_target.build_coupling_map().get_edges()) + ) + self.assertEqual( + { + (3, 4), + (4, 3), + (3, 1), + (1, 3), + (1, 2), + (2, 1), + (0, 1), + (1, 0), + }, + set(self.ibm_target.build_coupling_map().get_edges()), + ) + self.assertEqual(None, self.ideal_sim_target.build_coupling_map()) + + def test_coupling_map_2q_gate(self): + cmap = self.fake_backend_target.build_coupling_map("ecr") + self.assertEqual( + [ + (1, 0), + ], + cmap.get_edges(), + ) + + def test_coupling_map_3q_gate(self): + fake_target = Target() + ccx_props = { + (0, 1, 2): None, + (1, 0, 2): None, + (2, 1, 0): None, + } + fake_target.add_instruction(CCXGate(), ccx_props) + with self.assertLogs("qiskit.transpiler.target", level="WARN") as log: + cmap = fake_target.build_coupling_map() + self.assertEqual( + log.output, + [ + "WARNING:qiskit.transpiler.target:" + "This Target object contains multiqubit gates that " + "operate on > 2 qubits. This will not be reflected in " + "the output coupling map." + ], + ) + self.assertEqual([], cmap.get_edges()) + with self.assertRaises(ValueError): + fake_target.build_coupling_map("ccx") + + def test_physical_qubits(self): + self.assertEqual([], self.empty_target.physical_qubits) + self.assertEqual(list(range(5)), self.ibm_target.physical_qubits) + self.assertEqual(list(range(5)), self.aqt_target.physical_qubits) + self.assertEqual(list(range(2)), self.fake_backend_target.physical_qubits) + self.assertEqual(list(range(3)), self.ideal_sim_target.physical_qubits) + + def test_duplicate_instruction_add_instruction(self): + target = Target() + target.add_instruction(XGate(), {(0,): None}) + with self.assertRaises(AttributeError): + target.add_instruction(XGate(), {(1,): None}) + + def test_durations(self): + empty_durations = self.empty_target.durations() + self.assertEqual( + empty_durations.duration_by_name_qubits, InstructionDurations().duration_by_name_qubits + ) + aqt_durations = self.aqt_target.durations() + self.assertEqual(aqt_durations.duration_by_name_qubits, {}) + ibm_durations = self.ibm_target.durations() + expected = { + ("cx", (0, 1)): (5.1911e-07, "s"), + ("cx", (1, 0)): (5.5466e-07, "s"), + ("cx", (1, 2)): (2.2755e-07, "s"), + ("cx", (1, 3)): (4.9777e-07, "s"), + ("cx", (2, 1)): (2.6311e-07, "s"), + ("cx", (3, 1)): (4.6222e-07, "s"), + ("cx", (3, 4)): (2.7022e-07, "s"), + ("cx", (4, 3)): (3.0577e-07, "s"), + ("id", (0,)): (3.55e-08, "s"), + ("id", (1,)): (3.55e-08, "s"), + ("id", (2,)): (3.55e-08, "s"), + ("id", (3,)): (3.55e-08, "s"), + ("id", (4,)): (3.55e-08, "s"), + ("measure", (0,)): (5.813e-06, "s"), + ("measure", (1,)): (5.813e-06, "s"), + ("measure", (2,)): (5.813e-06, "s"), + ("measure", (3,)): (5.813e-06, "s"), + ("measure", (4,)): (5.813e-06, "s"), + ("rz", (0,)): (0, "s"), + ("rz", (1,)): (0, "s"), + ("rz", (2,)): (0, "s"), + ("rz", (3,)): (0, "s"), + ("rz", (4,)): (0, "s"), + ("sx", (0,)): (3.55e-08, "s"), + ("sx", (1,)): (3.55e-08, "s"), + ("sx", (2,)): (3.55e-08, "s"), + ("sx", (3,)): (3.55e-08, "s"), + ("sx", (4,)): (3.55e-08, "s"), + ("x", (0,)): (3.55e-08, "s"), + ("x", (1,)): (3.55e-08, "s"), + ("x", (2,)): (3.55e-08, "s"), + ("x", (3,)): (3.55e-08, "s"), + ("x", (4,)): (3.55e-08, "s"), + } + self.assertEqual(ibm_durations.duration_by_name_qubits, expected) + + def test_mapping(self): + with self.assertRaises(KeyError): + _res = self.empty_target["cx"] + expected = { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + } + self.assertEqual(self.aqt_target["r"], expected) + self.assertEqual(["rx", "ry", "rz", "r", "rxx", "measure"], list(self.aqt_target)) + expected_values = [ + { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + }, + { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + }, + { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + }, + { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + }, + { + (0, 1): None, + (0, 2): None, + (0, 3): None, + (0, 4): None, + (1, 0): None, + (2, 0): None, + (3, 0): None, + (4, 0): None, + (1, 2): None, + (1, 3): None, + (1, 4): None, + (2, 1): None, + (3, 1): None, + (4, 1): None, + (2, 3): None, + (2, 4): None, + (3, 2): None, + (4, 2): None, + (3, 4): None, + (4, 3): None, + }, + { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + }, + ] + self.assertEqual(expected_values, list(self.aqt_target.values())) + expected_items = { + "rx": { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + }, + "ry": { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + }, + "rz": { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + }, + "r": { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + }, + "rxx": { + (0, 1): None, + (0, 2): None, + (0, 3): None, + (0, 4): None, + (1, 0): None, + (2, 0): None, + (3, 0): None, + (4, 0): None, + (1, 2): None, + (1, 3): None, + (1, 4): None, + (2, 1): None, + (3, 1): None, + (4, 1): None, + (2, 3): None, + (2, 4): None, + (3, 2): None, + (4, 2): None, + (3, 4): None, + (4, 3): None, + }, + "measure": { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + }, + } + self.assertEqual(expected_items, dict(self.aqt_target.items())) + self.assertIn("cx", self.ibm_target) + self.assertNotIn("ecr", self.ibm_target) + self.assertEqual(len(self.ibm_target), 6) + + def test_update_instruction_properties(self): + self.aqt_target.update_instruction_properties( + "rxx", + (0, 1), + InstructionProperties(duration=1e-6, error=1e-5), + ) + self.assertEqual(self.aqt_target["rxx"][(0, 1)].duration, 1e-6) + self.assertEqual(self.aqt_target["rxx"][(0, 1)].error, 1e-5) + + def test_update_instruction_properties_invalid_instruction(self): + with self.assertRaises(KeyError): + self.ibm_target.update_instruction_properties("rxx", (0, 1), None) + + def test_update_instruction_properties_invalid_qarg(self): + with self.assertRaises(KeyError): + self.fake_backend_target.update_instruction_properties("ecr", (0, 1), None) + + def test_str(self): + expected = """Target +Number of qubits: 5 +Instructions: + id + (0,): + Duration: 3.55e-08 sec. + Error Rate: 0.000413 + (1,): + Duration: 3.55e-08 sec. + Error Rate: 0.000502 + (2,): + Duration: 3.55e-08 sec. + Error Rate: 0.0004003 + (3,): + Duration: 3.55e-08 sec. + Error Rate: 0.000614 + (4,): + Duration: 3.55e-08 sec. + Error Rate: 0.006149 + rz + (0,): + Duration: 0 sec. + Error Rate: 0 + (1,): + Duration: 0 sec. + Error Rate: 0 + (2,): + Duration: 0 sec. + Error Rate: 0 + (3,): + Duration: 0 sec. + Error Rate: 0 + (4,): + Duration: 0 sec. + Error Rate: 0 + sx + (0,): + Duration: 3.55e-08 sec. + Error Rate: 0.000413 + (1,): + Duration: 3.55e-08 sec. + Error Rate: 0.000502 + (2,): + Duration: 3.55e-08 sec. + Error Rate: 0.0004003 + (3,): + Duration: 3.55e-08 sec. + Error Rate: 0.000614 + (4,): + Duration: 3.55e-08 sec. + Error Rate: 0.006149 + x + (0,): + Duration: 3.55e-08 sec. + Error Rate: 0.000413 + (1,): + Duration: 3.55e-08 sec. + Error Rate: 0.000502 + (2,): + Duration: 3.55e-08 sec. + Error Rate: 0.0004003 + (3,): + Duration: 3.55e-08 sec. + Error Rate: 0.000614 + (4,): + Duration: 3.55e-08 sec. + Error Rate: 0.006149 + cx + (3, 4): + Duration: 2.7022e-07 sec. + Error Rate: 0.00713 + (4, 3): + Duration: 3.0577e-07 sec. + Error Rate: 0.00713 + (3, 1): + Duration: 4.6222e-07 sec. + Error Rate: 0.00929 + (1, 3): + Duration: 4.9777e-07 sec. + Error Rate: 0.00929 + (1, 2): + Duration: 2.2755e-07 sec. + Error Rate: 0.00659 + (2, 1): + Duration: 2.6311e-07 sec. + Error Rate: 0.00659 + (0, 1): + Duration: 5.1911e-07 sec. + Error Rate: 0.01201 + (1, 0): + Duration: 5.5466e-07 sec. + Error Rate: 0.01201 + measure + (0,): + Duration: 5.813e-06 sec. + Error Rate: 0.0751 + (1,): + Duration: 5.813e-06 sec. + Error Rate: 0.0225 + (2,): + Duration: 5.813e-06 sec. + Error Rate: 0.0146 + (3,): + Duration: 5.813e-06 sec. + Error Rate: 0.0215 + (4,): + Duration: 5.813e-06 sec. + Error Rate: 0.0333 +""" + self.assertEqual(expected, str(self.ibm_target)) + aqt_expected = """Target: AQT Target +Number of qubits: 5 +Instructions: + rx + (0,) + (1,) + (2,) + (3,) + (4,) + ry + (0,) + (1,) + (2,) + (3,) + (4,) + rz + (0,) + (1,) + (2,) + (3,) + (4,) + r + (0,) + (1,) + (2,) + (3,) + (4,) + rxx + (0, 1) + (0, 2) + (0, 3) + (0, 4) + (1, 0) + (2, 0) + (3, 0) + (4, 0) + (1, 2) + (1, 3) + (1, 4) + (2, 1) + (3, 1) + (4, 1) + (2, 3) + (2, 4) + (3, 2) + (4, 2) + (3, 4) + (4, 3) + measure + (0,) + (1,) + (2,) + (3,) + (4,) +""" + self.assertEqual(aqt_expected, str(self.aqt_target)) + sim_expected = """Target: Ideal Simulator +Number of qubits: 3 +Instructions: + u + rx + ry + rz + cx + ecr + ccx + measure +""" + self.assertEqual(sim_expected, str(self.ideal_sim_target)) + + def test_extra_props_str(self): + target = Target(description="Extra Properties") + + class ExtraProperties(InstructionProperties): + """An example properties subclass.""" + + def __init__( + self, + duration=None, + error=None, + calibration=None, + tuned=None, + diamond_norm_error=None, + ): + super().__init__(duration=duration, error=error, calibration=calibration) + self.tuned = tuned + self.diamond_norm_error = diamond_norm_error + + cx_props = { + (3, 4): ExtraProperties( + duration=270.22e-9, error=0.00713, tuned=False, diamond_norm_error=2.12e-6 + ), + } + target.add_instruction(CXGate(), cx_props) + expected = """Target: Extra Properties +Number of qubits: 5 +Instructions: + cx + (3, 4): + Duration: 2.7022e-07 sec. + Error Rate: 0.00713 +""" + self.assertEqual(expected, str(target)) + + def test_timing_constraints(self): + generated_constraints = self.aqt_target.timing_constraints() + expected_constraints = TimingConstraints() + for i in ["granularity", "min_length", "pulse_alignment", "acquire_alignment"]: + self.assertEqual( + getattr(generated_constraints, i), + getattr(expected_constraints, i), + f"Generated constraints differs from expected for attribute {i}" + f"{getattr(generated_constraints, i)}!={getattr(expected_constraints, i)}", + ) + + +class TestPulseTarget(QiskitTestCase): + def setUp(self): + super().setUp() + self.pulse_target = Target( + dt=3e-7, granularity=2, min_length=4, pulse_alignment=8, aquire_alignment=8 + ) + with pulse.build(name="sx_q0") as self.custom_sx_q0: + pulse.play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)) + with pulse.build(name="sx_q1") as self.custom_sx_q1: + pulse.play(pulse.Constant(100, 0.2), pulse.DriveChannel(1)) + sx_props = { + (0,): InstructionProperties( + duration=35.5e-9, error=0.000413, calibration=self.custom_sx_q0 + ), + (1,): InstructionProperties( + duration=35.5e-9, error=0.000502, calibration=self.custom_sx_q1 + ), + } + self.pulse_target.add_instruction(SXGate(), sx_props) + + def test_instruction_schedule_map(self): + inst_map = self.pulse_target.instruction_schedule_map() + self.assertIn("sx", inst_map.instructions) + self.assertEqual(inst_map.qubits_with_instruction("sx"), [0, 1]) + self.assertTrue("sx" in inst_map.qubit_instructions(0)) + + def test_instruction_schedule_map_ideal_sim_backend(self): + ideal_sim_target = Target(num_qubits=3) + theta = Parameter("theta") + phi = Parameter("phi") + lam = Parameter("lambda") + for inst in [ + UGate(theta, phi, lam), + RXGate(theta), + RYGate(theta), + RZGate(theta), + CXGate(), + ECRGate(), + CCXGate(), + Measure(), + ]: + ideal_sim_target.add_instruction(inst, {None: None}) + inst_map = ideal_sim_target.instruction_schedule_map() + self.assertEqual(InstructionScheduleMap(), inst_map) + + def test_str(self): + expected = """Target +Number of qubits: 2 +Instructions: + sx + (0,): + Duration: 3.55e-08 sec. + Error Rate: 0.000413 + With pulse schedule calibration + (1,): + Duration: 3.55e-08 sec. + Error Rate: 0.000502 + With pulse schedule calibration +""" + self.assertEqual(expected, str(self.pulse_target)) + + def test_update_from_instruction_schedule_map_add_instruction(self): + target = Target() + inst_map = InstructionScheduleMap() + inst_map.add("sx", 0, self.custom_sx_q0) + inst_map.add("sx", 1, self.custom_sx_q1) + target.update_from_instruction_schedule_map(inst_map, {"sx": SXGate()}) + self.assertEqual(inst_map, target.instruction_schedule_map()) + + def test_update_from_instruction_schedule_map_update_schedule(self): + self.pulse_target.dt = None + inst_map = InstructionScheduleMap() + with pulse.build(name="sx_q1") as custom_sx: + pulse.play(pulse.Constant(1000, 0.2), pulse.DriveChannel(1)) + + inst_map.add("sx", 0, self.custom_sx_q0) + inst_map.add("sx", 1, custom_sx) + self.pulse_target.update_from_instruction_schedule_map(inst_map, {"sx": SXGate()}) + self.assertEqual(inst_map, self.pulse_target.instruction_schedule_map()) + self.assertIsNone(self.pulse_target["sx"][(0,)].duration) + self.assertIsNone(self.pulse_target["sx"][(0,)].error) + self.assertIsNone(self.pulse_target["sx"][(1,)].duration) + self.assertIsNone(self.pulse_target["sx"][(1,)].error) + + def test_update_from_instruction_schedule_map_new_instruction_no_name_map(self): + target = Target() + inst_map = InstructionScheduleMap() + inst_map.add("sx", 0, self.custom_sx_q0) + inst_map.add("sx", 1, self.custom_sx_q1) + with self.assertRaises(ValueError): + target.update_from_instruction_schedule_map(inst_map) + + def test_update_from_instruction_schedule_map_new_qarg_raises(self): + inst_map = InstructionScheduleMap() + inst_map.add("sx", 0, self.custom_sx_q0) + inst_map.add("sx", 1, self.custom_sx_q1) + inst_map.add("sx", 2, self.custom_sx_q1) + with self.assertRaises(KeyError): + self.pulse_target.update_from_instruction_schedule_map(inst_map) + + def test_update_from_instruction_schedule_map_with_dt_set(self): + inst_map = InstructionScheduleMap() + with pulse.build(name="sx_q1") as custom_sx: + pulse.play(pulse.Constant(1000, 0.2), pulse.DriveChannel(1)) + + inst_map.add("sx", 0, self.custom_sx_q0) + inst_map.add("sx", 1, custom_sx) + self.pulse_target.dt = 1.0 + self.pulse_target.update_from_instruction_schedule_map(inst_map, {"sx": SXGate()}) + self.assertEqual(inst_map, self.pulse_target.instruction_schedule_map()) + self.assertEqual(self.pulse_target["sx"][(1,)].duration, 1000.0) + self.assertIsNone(self.pulse_target["sx"][(1,)].error) + self.assertIsNone(self.pulse_target["sx"][(0,)].error) + + def test_update_from_instruction_schedule_map_with_error_dict(self): + inst_map = InstructionScheduleMap() + with pulse.build(name="sx_q1") as custom_sx: + pulse.play(pulse.Constant(1000, 0.2), pulse.DriveChannel(1)) + + inst_map.add("sx", 0, self.custom_sx_q0) + inst_map.add("sx", 1, custom_sx) + self.pulse_target.dt = 1.0 + error_dict = {"sx": {(1,): 1.0}} + + self.pulse_target.update_from_instruction_schedule_map( + inst_map, {"sx": SXGate()}, error_dict=error_dict + ) + self.assertEqual(self.pulse_target["sx"][(1,)].error, 1.0) + self.assertIsNone(self.pulse_target["sx"][(0,)].error) + + def test_timing_constraints(self): + generated_constraints = self.pulse_target.timing_constraints() + expected_constraints = TimingConstraints(2, 4, 8, 8) + for i in ["granularity", "min_length", "pulse_alignment", "acquire_alignment"]: + self.assertEqual( + getattr(generated_constraints, i), + getattr(expected_constraints, i), + f"Generated constraints differs from expected for attribute {i}" + f"{getattr(generated_constraints, i)}!={getattr(expected_constraints, i)}", + ) + + +class TestInstructionProperties(QiskitTestCase): + def test_empty_repr(self): + properties = InstructionProperties() + self.assertEqual( + repr(properties), + "InstructionProperties(duration=None, error=None, calibration=None)", + )