From 3694fff80f647e63b170beaaba1da4a2f76c9a2d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 1 Dec 2021 09:25:20 -0500 Subject: [PATCH] Add BackendV2 abstract class (#5885) * Add BackendV2 abstract class This commit adds a new abstract class that adds a new version of the backend abstract class. The primary changes being made in this class are that instead of using BackendConfiguration for immutable characteristics of the backend those are instead exposed as read only attributes of the Backend object itself. This also takes some time to improve how we represent some core data about a backend. The primary example of this is that basis gates are no longer a list of names but instead a list of Gate objects. * Adjust GateMap to be a dict instead of a graph This commit reworks the GateMap class to be an internal dict of gate instances to qargs (with optional properties) instead of building a graph. The graph representation was inherently limited in that it couldn't describe multiqubit gates involving > 2 qubits. * Add fields from properties to v2 backend interface This commit finishes by adding the defined optional methods for querying the equivalent of the backend properties on the new backend v2 interface. The per gate properties were already defined in the gate map class as optional fields for each gate entry so this defines methods for the per qubit fields (t2, t1, freq, readout error and duration). While not every backend supports these fields they're common ones that most could implement (assuming the data is available). For any other backend properties they can be custom fieldss in a subclass. * Adjust GateMap structure * Rename GateMap -> Target and drop readout error (as it'll be per instruction error) * Add instruction durations method to target * Add optional dt properties to backend object * Add instruction durations method to backendv2 * Fix lint * Rename max_experiments -> max_circuits * Add two_q_gate option to Target.coupling_map() * Add option for a list of qubits to t1, t2, and frequency * Change list return to numpy array for t1 and t2 * Drop qubit frequency * Add pulse to instruction durations * Update units for dt and dtm * Update qiskit/transpiler/target.py Co-authored-by: Naoki Kanazawa * Fix type hints for t1 and t2 * Fix transpile() to actually use BackendV2 correctly * Add transpile() test case with BackendV2 * Add repr for InstructionProperties * Tweak docs * Replace get_qargs_from_name with get_instructions_for_qarg * Add tests for target * Fix black's insane formatting choice * Add more target tests * Cleanup debug prints * Make Target a mapping * Rename get_gate_from_name to get_instruction_from_name * Fix test issue with rename of get_gate_from_name * Add support for running schedule() with a backend v2 * Add method to update instruction properties in target * Fix caching * Add tests for new target methods * Fix lint * Add support for qargs being None in ideal simulator case This commit adds support for a backend setting the qargs on an instruction to None as a shorthand for everything. This is useful for things like ideal simulator backends that don't have any constraints on which qubits an instruction can run on and just have a set of supported instructions and a number of qubits supported by the simulator. * Add test for ideal sim target instruction_schedule_map() method * Add release note * Add BackendV2 to autodoc * Drop distance() and distance() matrix from Target * Fix leftovers in doc strings * Add __str__ for target * Add logging when instruction_names includes instructions not on all qubits * Documentation fixes from code review Co-authored-by: Naoki Kanazawa * Use seconds everywhere * Fixes from recent changes * Add update_from_instruction_schedule_map() method * Use defaultdict for _qarg_gate_map in target * Explicitly add ScheduleBlock to backend.run() * Tweak docs for target * Add pulse channel abstract methods to BackendV2 class * Fix lint * Improve target's update_from_instruction_schedule_map * Add timing constraints to the target * Fix typos in release note Co-authored-by: Ali Javadi-Abhari * Re-export FakeBackendV2 in qiskit.test.mock * Update target docstring example * Make properties optional on Target.add_instruction() For the case of ideal simulators for each instruction they would have to call something like add_instruction(XGate(), {None: None} which is tedious for every instruction. This commit makes the properties argument optional and if not specified default to {None: None} this enables ideal simulators to just call add_instruction(XGate()) for each instruction the simulator supports. * Add docstring type to BackendV2.target attribute * Make Target.instructions property return tuple and add operations * Rename instruction property schedule -> calibration * Add optional backend metadata as constructor kwargs * More instruction->Operation renames * Remove conditional and max_shots abstract properties The conditional property isn't needed and should be included in the target. The max_shots property should be expressed in the Option as bounds or validation on setting. * Add optional validators to Options object This commit adds optional validators for the Options object. This lets a user constructing an Options object (typically a Backend/provider author) optionally set bounds for numeric values, types, or valid choices for any field in the Options object. This will be validated when the value is updated and shown in a human readable format in the __str__ output for the Options object. * Include dt in output InstructionDurations from Target Co-authored-by: itoko <15028342+itoko@users.noreply.github.com> * Add QubitProperties class and qubits() backend method This commit adds a new optional method to BackendV2 which when defined by a BackendV2 subclass returns a QubitProperties class for the specified Qubits. QubitProperties is a new data container class for the properties of a qubit such as t1, t2 and frequency. This replaces the previous optional methods t1 and t2 as the storage location for these properties. Those methods are updated to work using qubits() directly. The tests are also updated to validate these methods work as expected. * Add instruction_properties() method * Fix lint in mock backend * Make provider backref optional and fix docstring * Rename BackendV2.qubits() BackendV2.qubit_properties() * Add note to QubitProperties docstring on subclassing * Fix Options.set_validator() docstring * Add missing **fields docstring to BackendV2 init This is just copied from BackendV1 which has the same argument on it's constructor. * Remove t1/t2 methods from BackendV2 class * Remove unused imports * Remove properties field from Properties classes * Apply suggestions from code review Co-authored-by: Ali Javadi-Abhari * Rename InstructionProperties.length -> duration * Docstring improvements and more instruction->operation * Update releasenotes/notes/add-backend-v2-ce84c976fb13b038.yaml Co-authored-by: Kevin Krsulich * Rename and fix incomplete basis computation * Tweak release note wording on custom attributes of backend Co-authored-by: Kevin Krsulich * Finish set_validator docstring on options class * Update qiskit/providers/backend.py Co-authored-by: Kevin Krsulich * Apply suggestions from code review This commit makes several changes found in code review. It fixes a typo with __slots__ for QubitProperties, fixes string formatting for a raised error, updates a list comprehension to a more performant syntax, and fixes release note formatting. Co-authored-by: Jake Lishman * Return dict .keys() directly for Target properties This commit updates the return of a couple property methods on the Target class to return dict.keys() instead of set(dict) as the keys return type implements the abstract Set collections interface and returns just a read only view of the dict's keys instead of a mutable copy. For this api the lower overhead and read-only nature are better choices than returning a set copy. Co-authored-by: Jake Lishman * Fix stray use of properties attr in QubitProperties * Update stray set() return for property * Apply suggestions from code review Co-authored-by: Kevin Krsulich * Rename Target.get_qargs() -> qargs_for_operation_name() * Fix lint * Rename Target.coupling_map() -> build_coupling_map() * Fix test name for discovery * qarg -> qargs in target method arguments * Add target to docs build * Fix target docs * Fix lint * Tweak documentation for target to make intent and future clear * Fix typo in QubitProperties docstring Co-authored-by: Rathish Cholarajan Co-authored-by: Naoki Kanazawa Co-authored-by: Ali Javadi-Abhari Co-authored-by: itoko <15028342+itoko@users.noreply.github.com> Co-authored-by: Kevin Krsulich Co-authored-by: Jake Lishman Co-authored-by: Rathish Cholarajan --- qiskit/compiler/scheduler.py | 46 +- qiskit/compiler/transpiler.py | 107 +- qiskit/providers/__init__.py | 4 + qiskit/providers/backend.py | 392 +++++++ qiskit/providers/options.py | 119 +- qiskit/test/mock/__init__.py | 1 + qiskit/test/mock/fake_backend_v2.py | 97 ++ qiskit/transpiler/__init__.py | 11 + qiskit/transpiler/target.py | 658 +++++++++++ .../add-backend-v2-ce84c976fb13b038.yaml | 87 ++ test/python/providers/test_backend_v2.py | 68 ++ test/python/providers/test_options.py | 107 ++ test/python/transpiler/test_target.py | 1042 +++++++++++++++++ 13 files changed, 2685 insertions(+), 54 deletions(-) create mode 100644 qiskit/test/mock/fake_backend_v2.py create mode 100644 qiskit/transpiler/target.py create mode 100644 releasenotes/notes/add-backend-v2-ce84c976fb13b038.yaml create mode 100644 test/python/providers/test_backend_v2.py create mode 100644 test/python/providers/test_options.py create mode 100644 test/python/transpiler/test_target.py 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)", + )