Skip to content

Commit

Permalink
Add constructor to build target from legacy transpiler model (#9255)
Browse files Browse the repository at this point in the history
* Add constructor to build target from legacy transpiler model

This commit adds a new constructor method from_configuration() to the
Target class. This can be used to construct a new Target object from the
legacy input types: basis_gates, coupling_map, etc. This can then be
used to provide a conversion layer in transpile() in support of
migrating to using the target everywhere internally.

* Add more test coverage

* Apply suggestions from code review

Co-authored-by: Kevin Krsulich <kevin@krsulich.net>

* Make num_qubits optional

* Explain arg precedence in docstring

* Use assertRaisesRegex

* Fix lint

* Fix rebase name mismatch

* Fix lint

* Update error message with multiqubit gates

* Give instruction durations higher priority

* Improve handling of instruction schedule map

* Add comment about how connectivity is defined

* Update test exception assertion regex for new error message

* Update qiskit/transpiler/target.py

Co-authored-by: Naoki Kanazawa <nkanazawa1989@gmail.com>

---------

Co-authored-by: Kevin Krsulich <kevin@krsulich.net>
Co-authored-by: Naoki Kanazawa <nkanazawa1989@gmail.com>
  • Loading branch information
3 people committed Apr 13, 2023
1 parent 9157b40 commit d07c5cc
Show file tree
Hide file tree
Showing 3 changed files with 365 additions and 2 deletions.
241 changes: 240 additions & 1 deletion qiskit/transpiler/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@
A target object represents the minimum set of information the transpiler needs
from a backend
"""

from __future__ import annotations


import warnings
from typing import Tuple, Union

from typing import Tuple, Union, Optional, Dict, List, Any
from collections.abc import Mapping
from collections import defaultdict
import datetime
Expand All @@ -38,6 +43,8 @@
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.instruction_durations import InstructionDurations
from qiskit.transpiler.timing_constraints import TimingConstraints
from qiskit.providers.exceptions import BackendPropertyError
from qiskit.pulse.exceptions import PulseError
from qiskit.utils.deprecation import deprecate_arguments
from qiskit.exceptions import QiskitError

Expand All @@ -46,6 +53,7 @@
from qiskit.providers.backend import QubitProperties # pylint: disable=unused-import
from qiskit.providers.models.backendproperties import BackendProperties


logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -1176,6 +1184,237 @@ def __str__(self):
output.write("".join(prop_str_pieces))
return output.getvalue()

@classmethod
def from_configuration(
cls,
basis_gates: List[str],
num_qubits: Optional[int] = None,
coupling_map: Optional[CouplingMap] = None,
inst_map: Optional[InstructionScheduleMap] = None,
backend_properties: Optional[BackendProperties] = None,
instruction_durations: Optional[InstructionDurations] = None,
dt: Optional[float] = None,
timing_constraints: Optional[TimingConstraints] = None,
custom_name_mapping: Optional[Dict[str, Any]] = None,
) -> Target:
"""Create a target object from the individual global configuration
Prior to the creation of the :class:`~.Target` class, the constraints
of a backend were represented by a collection of different objects
which combined represent a subset of the information contained in
the :class:`~.Target`. This function provides a simple interface
to convert those separate objects to a :class:`~.Target`.
This constructor will use the input from ``basis_gates``, ``num_qubits``,
and ``coupling_map`` to build a base model of the backend and the
``instruction_durations``, ``backend_properties``, and ``inst_map`` inputs
are then queried (in that order) based on that model to look up the properties
of each instruction and qubit. If there is an inconsistency between the inputs
any extra or conflicting information present in ``instruction_durations``,
``backend_properties``, or ``inst_map`` will be ignored.
Args:
basis_gates: The list of basis gate names for the backend. For the
target to be created these names must either be in the output
from :func:~.get_standard_gate_name_mapping` or present in the
specified ``custom_name_mapping`` argument.
num_qubits: The number of qubits supported on the backend.
coupling_map: The coupling map representing connectivity constraints
on the backend. If specified all gates from ``basis_gates`` will
be supported on all qubits (or pairs of qubits).
inst_map: The instruction schedule map representing the pulse
:class:`~.Schedule` definitions for each instruction. If this
is specified ``coupling_map`` must be specified. The
``coupling_map`` is used as the source of truth for connectivity
and if ``inst_map`` is used the schedule is looked up based
on the instuctions from the pair of ``basis_gates`` and
``coupling_map``. If you want to define a custom gate for
a particular qubit or qubit pair, you can manually build :class:`.Target`.
backend_properties: The :class:`~.BackendProperties` object which is
used for instruction properties and qubit properties.
If specified and instruction properties are intended to be used
then the ``coupling_map`` argument must be specified. This is
only used to lookup error rates and durations (unless
``instruction_durations`` is specified which would take
precedence) for instructions specified via ``coupling_map`` and
``basis_gates``.
instruction_durations: Optional instruction durations for instructions. If specified
it will take priority for setting the ``duration`` field in the
:class:`~InstructionProperties` objects for the instructions in the target.
dt: The system time resolution of input signals in seconds
timing_constraints: Optional timing constraints to include in the
:class:`~.Target`
custom_name_mapping: An optional dictionary that maps custom gate/operation names in
``basis_gates`` to an :class:`~.Operation` object representing that
gate/operation. By default most standard gates names are mapped to the
standard gate object from :mod:`qiskit.circuit.library` this only needs
to be specified if the input ``basis_gates`` defines gates in names outside
that set.
Returns:
Target: the target built from the input configuration
Raises:
TranspilerError: If the input basis gates contain > 2 qubits and ``coupling_map`` is
specified.
KeyError: If no mappign is available for a specified ``basis_gate``.
"""
granularity = 1
min_length = 1
pulse_alignment = 1
acquire_alignment = 1
if timing_constraints is not None:
granularity = timing_constraints.granularity
min_length = timing_constraints.min_length
pulse_alignment = timing_constraints.pulse_alignment
acquire_alignment = timing_constraints.acquire_alignment

qubit_properties = None
if backend_properties is not None:
# pylint: disable=cyclic-import
from qiskit.providers.backend_compat import qubit_props_list_from_props

qubit_properties = qubit_props_list_from_props(properties=backend_properties)

target = cls(
num_qubits=num_qubits,
dt=dt,
granularity=granularity,
min_length=min_length,
pulse_alignment=pulse_alignment,
acquire_alignment=acquire_alignment,
qubit_properties=qubit_properties,
)
name_mapping = get_standard_gate_name_mapping()
if custom_name_mapping is not None:
name_mapping.update(custom_name_mapping)

# While BackendProperties can also contain coupling information we
# rely solely on CouplingMap to determin connectivity. This is because
# in legacy transpiler usage (and implicitly in the BackendV1 data model)
# the coupling map is used to define connecitivity constraints and
# the properties is only used for error rate and duration population.
# If coupling map is not specified we ignore the backend_properties
if coupling_map is None:
for gate in basis_gates:
if gate not in name_mapping:
raise KeyError(
f"The specified basis gate: {gate} is not present in the standard gate "
"names or a provided custom_name_mapping"
)
target.add_instruction(name_mapping[gate], name=gate)
else:
one_qubit_gates = []
two_qubit_gates = []
global_ideal_variable_width_gates = [] # pylint: disable=invalid-name
if num_qubits is None:
num_qubits = len(coupling_map.graph)
for gate in basis_gates:
if gate not in name_mapping:
raise KeyError(
f"The specified basis gate: {gate} is not present in the standard gate "
"names or a provided custom_name_mapping"
)
gate_obj = name_mapping[gate]
if gate_obj.num_qubits == 1:
one_qubit_gates.append(gate)
elif gate_obj.num_qubits == 2:
two_qubit_gates.append(gate)
elif inspect.isclass(gate_obj):
global_ideal_variable_width_gates.append(gate)
else:
raise TranspilerError(
f"The specified basis gate: {gate} has {gate_obj.num_qubits} "
"qubits. This constructor method only supports fixed width operations "
"with <= 2 qubits (because connectivity is defined on a CouplingMap)."
)
for gate in one_qubit_gates:
gate_properties = {}
for qubit in range(num_qubits):
error = None
duration = None
calibration = None
if backend_properties is not None:
if duration is None:
try:
duration = backend_properties.gate_length(gate, qubit)
except BackendPropertyError:
duration = None
try:
error = backend_properties.gate_error(gate, qubit)
except BackendPropertyError:
error = None
if inst_map is not None:
try:
calibration = inst_map._get_calibration_entry(gate, qubit)
# If we have dt defined and there is a custom calibration which is user
# generate use that custom pulse schedule for the duration. If it is
# not user generated than we assume it's the same duration as what is
# defined in the backend properties
if dt and calibration.user_provided:
duration = calibration.get_schedule().duration * dt
except PulseError:
calibration = None
# Durations if specified manually should override model objects
if instruction_durations is not None:
try:
duration = instruction_durations.get(gate, qubit, unit="s")
except TranspilerError:
duration = None

if error is None and duration is None and calibration is None:
gate_properties[(qubit,)] = None
else:
gate_properties[(qubit,)] = InstructionProperties(
duration=duration, error=error, calibration=calibration
)
target.add_instruction(name_mapping[gate], properties=gate_properties, name=gate)
edges = list(coupling_map.get_edges())
for gate in two_qubit_gates:
gate_properties = {}
for edge in edges:
error = None
duration = None
calibration = None
if backend_properties is not None:
if duration is None:
try:
duration = backend_properties.gate_length(gate, edge)
except BackendPropertyError:
duration = None
try:
error = backend_properties.gate_error(gate, edge)
except BackendPropertyError:
error = None
if inst_map is not None:
try:
calibration = inst_map._get_calibration_entry(gate, edge)
# If we have dt defined and there is a custom calibration which is user
# generate use that custom pulse schedule for the duration. If it is
# not user generated than we assume it's the same duration as what is
# defined in the backend properties
if dt and calibration.user_provided:
duration = calibration.get_schedule().duration * dt
except PulseError:
calibration = None
# Durations if specified manually should override model objects
if instruction_durations is not None:
try:
duration = instruction_durations.get(gate, edge, unit="s")
except TranspilerError:
duration = None

if error is None and duration is None and calibration is None:
gate_properties[edge] = None
else:
gate_properties[edge] = InstructionProperties(
duration=duration, error=error, calibration=calibration
)
target.add_instruction(name_mapping[gate], properties=gate_properties, name=gate)
for gate in global_ideal_variable_width_gates:
target.add_instruction(name_mapping[gate], name=gate)
return target


def target_to_backend_properties(target: Target):
"""Convert a :class:`~.Target` object into a legacy :class:`~.BackendProperties`"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
features:
- |
Added a new constructor for the :class:`~.Target` class,
:meth:`.Target.from_configuration`, which lets you construct a
:class:`~.Target` object from the separate object types for describing
the constraints of a backend (e.g. basis gates, :class:`~.CouplingMap`,
:class:`~.BackendProperties`, etc). For example::
target = Target.from_configuration(
basis_gates=["u", "cx", "measure"],
coupling_map=CouplingMap.from_line(25),
)
This will construct a :class:`~.Target` object that has :class:`~.UGate`,
:class:`~.CXGate`, and :class:`~.Measure` globally available on 25 qubits
which are connected in a line.
Loading

0 comments on commit d07c5cc

Please sign in to comment.