Skip to content

Commit

Permalink
Remove use of BackendProperties (BackendV1) in transpiler pipeline (Q…
Browse files Browse the repository at this point in the history
…iskit#13722)

* Remove `BackendProperties` from transpilation pipeline, remove unit tests that depend on BackendV1.

* Remove coupling_map input in VF2PostLayout pass, as it would only be used if backend properties were present. After the removal of the latter, the coupling_map would always be overwritten by the target.
  • Loading branch information
ElePT authored Feb 18, 2025
1 parent 3aa8cc1 commit d9b8a18
Show file tree
Hide file tree
Showing 15 changed files with 120 additions and 680 deletions.
26 changes: 2 additions & 24 deletions qiskit/compiler/transpiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from qiskit.dagcircuit import DAGCircuit
from qiskit.providers.backend import Backend
from qiskit.providers.backend_compat import BackendV2Converter
from qiskit.providers.models.backendproperties import BackendProperties
from qiskit.pulse import Schedule, InstructionScheduleMap
from qiskit.transpiler import Layout, CouplingMap, PropertySet
from qiskit.transpiler.basepasses import BasePass
Expand Down Expand Up @@ -58,22 +57,13 @@
"with defined timing constraints with "
"`Target.from_configuration(..., timing_constraints=...)`",
)
@deprecate_arg(
name="backend_properties",
since="1.3",
package_name="Qiskit",
removal_timeline="in Qiskit 2.0",
additional_msg="The `target` parameter should be used instead. You can build a `Target` instance "
"with defined properties with Target.from_configuration(..., backend_properties=...)",
)
@deprecate_pulse_arg("inst_map", predicate=lambda inst_map: inst_map is not None)
def transpile( # pylint: disable=too-many-return-statements
circuits: _CircuitT,
backend: Optional[Backend] = None,
basis_gates: Optional[List[str]] = None,
inst_map: Optional[List[InstructionScheduleMap]] = None,
coupling_map: Optional[Union[CouplingMap, List[List[int]]]] = None,
backend_properties: Optional[BackendProperties] = None,
initial_layout: Optional[Union[Layout, Dict, List]] = None,
layout_method: Optional[str] = None,
routing_method: Optional[str] = None,
Expand Down Expand Up @@ -105,7 +95,7 @@ def transpile( # pylint: disable=too-many-return-statements
The prioritization of transpilation target constraints works as follows: if a ``target``
input is provided, it will take priority over any ``backend`` input or loose constraints
(``basis_gates``, ``inst_map``, ``coupling_map``, ``backend_properties``, ``instruction_durations``,
(``basis_gates``, ``inst_map``, ``coupling_map``, ``instruction_durations``,
``dt`` or ``timing_constraints``). If a ``backend`` is provided together with any loose constraint
from the list above, the loose constraint will take priority over the corresponding backend
constraint. This behavior is independent of whether the ``backend`` instance is of type
Expand All @@ -123,7 +113,6 @@ def transpile( # pylint: disable=too-many-return-statements
**inst_map** target inst_map inst_map
**dt** target dt dt
**timing_constraints** target timing_constraints timing_constraints
**backend_properties** target backend_properties backend_properties
============================ ========= ======================== =======================
Args:
Expand All @@ -148,10 +137,6 @@ def transpile( # pylint: disable=too-many-return-statements
#. List, must be given as an adjacency matrix, where each entry
specifies all directed two-qubit interactions supported by backend,
e.g: ``[[0, 1], [0, 3], [1, 2], [1, 5], [2, 5], [4, 1], [5, 3]]``
backend_properties: properties returned by a backend, including information on gate
errors, readout errors, qubit coherence times, etc. Find a backend
that provides this information with: ``backend.properties()``
initial_layout: Initial position of virtual qubits on physical qubits.
If this layout makes the circuit compatible with the coupling_map
constraints, it will be used. The final layout is not guaranteed to be the same,
Expand Down Expand Up @@ -394,7 +379,7 @@ def callback_func(**kwargs):
# Edge cases require using the old model (loose constraints) instead of building a target,
# but we don't populate the passmanager config with loose constraints unless it's one of
# the known edge cases to control the execution path.
# Filter instruction_durations, timing_constraints, backend_properties and inst_map deprecation
# Filter instruction_durations, timing_constraints and inst_map deprecation
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore",
Expand All @@ -414,20 +399,13 @@ def callback_func(**kwargs):
message=".*``instruction_durations`` is deprecated as of Qiskit 1.3.*",
module="qiskit",
)
warnings.filterwarnings(
"ignore",
category=DeprecationWarning,
message=".*``backend_properties`` is deprecated as of Qiskit 1.3.*",
module="qiskit",
)
pm = generate_preset_pass_manager(
optimization_level,
target=target,
backend=backend,
basis_gates=basis_gates,
coupling_map=coupling_map,
instruction_durations=instruction_durations,
backend_properties=backend_properties,
timing_constraints=timing_constraints,
inst_map=inst_map,
initial_layout=initial_layout,
Expand Down
29 changes: 2 additions & 27 deletions qiskit/transpiler/passes/layout/dense_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,15 @@ class DenseLayout(AnalysisPass):
by being set in ``property_set``.
"""

def __init__(self, coupling_map=None, backend_prop=None, target=None):
def __init__(self, coupling_map=None, target=None):
"""DenseLayout initializer.
Args:
coupling_map (Coupling): directed graph representing a coupling map.
backend_prop (BackendProperties): backend properties object
target (Target): A target representing the target backend.
"""
super().__init__()
self.coupling_map = coupling_map
self.backend_prop = backend_prop
self.target = target
self.adjacency_matrix = None
if target is not None:
Expand Down Expand Up @@ -127,8 +125,6 @@ def _best_subset(self, num_qubits, num_meas, num_cx, coupling_map):
error_mat, use_error = _build_error_matrix(
coupling_map.size(),
reverse_index_map,
backend_prop=self.backend_prop,
coupling_map=self.coupling_map,
target=self.target,
)

Expand All @@ -148,7 +144,7 @@ def _best_subset(self, num_qubits, num_meas, num_cx, coupling_map):
return best_map


def _build_error_matrix(num_qubits, qubit_map, target=None, coupling_map=None, backend_prop=None):
def _build_error_matrix(num_qubits, qubit_map, target=None):
error_mat = np.zeros((num_qubits, num_qubits))
use_error = False
if target is not None and target.qargs is not None:
Expand Down Expand Up @@ -178,25 +174,4 @@ def _build_error_matrix(num_qubits, qubit_map, target=None, coupling_map=None, b
elif len(qargs) == 2:
error_mat[qubit_map[qargs[0]]][qubit_map[qargs[1]]] = max_error
use_error = True
elif backend_prop and coupling_map:
error_dict = {
tuple(gate.qubits): gate.parameters[0].value
for gate in backend_prop.gates
if len(gate.qubits) == 2
}
for edge in coupling_map.get_edges():
gate_error = error_dict.get(edge)
if gate_error is not None:
if edge[0] not in qubit_map or edge[1] not in qubit_map:
continue
error_mat[qubit_map[edge[0]]][qubit_map[edge[1]]] = gate_error
use_error = True
for index, qubit_data in enumerate(backend_prop.qubits):
if index not in qubit_map:
continue
for item in qubit_data:
if item.name == "readout_error":
mapped_index = qubit_map[index]
error_mat[mapped_index][mapped_index] = item.value
use_error = True
return error_mat, use_error
11 changes: 2 additions & 9 deletions qiskit/transpiler/passes/layout/vf2_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ def __init__(
seed=None,
call_limit=None,
time_limit=None,
properties=None,
max_trials=None,
target=None,
):
Expand All @@ -94,16 +93,13 @@ def __init__(
call_limit (int): The number of state visits to attempt in each execution of
VF2.
time_limit (float): The total time limit in seconds to run ``VF2Layout``
properties (BackendProperties): The backend properties for the backend. If
:meth:`~qiskit.providers.models.BackendProperties.readout_error` is available
it is used to score the layout.
max_trials (int): The maximum number of trials to run VF2 to find
a layout. If this is not specified the number of trials will be limited
based on the number of edges in the interaction graph or the coupling graph
(whichever is larger) if no other limits are set. If set to a value <= 0 no
limit on the number of trials will be set.
target (Target): A target representing the backend device to run ``VF2Layout`` on.
If specified it will supersede a set value for ``properties`` and
If specified it will supersede a set value for
``coupling_map`` if the :class:`.Target` contains connectivity constraints. If the value
of ``target`` models an ideal backend without any constraints then the value of
``coupling_map``
Expand All @@ -121,7 +117,6 @@ def __init__(
self.coupling_map = target_coupling_map
else:
self.coupling_map = coupling_map
self.properties = properties
self.strict_direction = strict_direction
self.seed = seed
self.call_limit = call_limit
Expand All @@ -135,9 +130,7 @@ def run(self, dag):
raise TranspilerError("coupling_map or target must be specified.")
self.avg_error_map = self.property_set["vf2_avg_error_map"]
if self.avg_error_map is None:
self.avg_error_map = vf2_utils.build_average_error_map(
self.target, self.properties, self.coupling_map
)
self.avg_error_map = vf2_utils.build_average_error_map(self.target, self.coupling_map)

result = vf2_utils.build_interaction_graph(dag, self.strict_direction)
if result is None:
Expand Down
140 changes: 60 additions & 80 deletions qiskit/transpiler/passes/layout/vf2_post_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ class VF2PostLayout(AnalysisPass):
* ``">2q gates in basis"``: If VF2PostLayout can't work with the basis of the circuit.
By default, this pass will construct a heuristic scoring map based on
the error rates in the provided ``target`` (or ``properties`` if ``target``
is not provided). However, analysis passes can be run prior to this pass
the error rates in the provided ``target``. However, analysis passes can be run prior to this pass
and set ``vf2_avg_error_map`` in the property set with a :class:`~.ErrorMap`
instance. If a value is ``NaN`` that is treated as an ideal edge
For example if an error map is created as::
Expand All @@ -102,8 +101,6 @@ class VF2PostLayout(AnalysisPass):
def __init__(
self,
target=None,
coupling_map=None,
properties=None,
seed=None,
call_limit=None,
time_limit=None,
Expand All @@ -114,12 +111,6 @@ def __init__(
Args:
target (Target): A target representing the backend device to run ``VF2PostLayout`` on.
If specified it will supersede a set value for ``properties`` and
``coupling_map``.
coupling_map (CouplingMap): Directed graph representing a coupling map.
properties (BackendProperties): The backend properties for the backend. If
:meth:`~qiskit.providers.models.BackendProperties.readout_error` is available
it is used to score the layout.
seed (int): Sets the seed of the PRNG. -1 Means no node shuffling.
call_limit (int): The number of state visits to attempt in each execution of
VF2.
Expand All @@ -138,12 +129,10 @@ def __init__(
a layout. A value of ``0`` (the default) means 'unlimited'.
Raises:
TypeError: At runtime, if neither ``coupling_map`` or ``target`` are provided.
TypeError: At runtime, if ``target`` isn't provided.
"""
super().__init__()
self.target = target
self.coupling_map = coupling_map
self.properties = properties
self.call_limit = call_limit
self.time_limit = time_limit
self.max_trials = max_trials
Expand All @@ -153,16 +142,12 @@ def __init__(

def run(self, dag):
"""run the layout method"""
if self.target is None and (self.coupling_map is None or self.properties is None):
raise TranspilerError(
"A target must be specified or a coupling map and properties must be provided"
)
if self.target is None:
raise TranspilerError("A target must be specified or a coupling map must be provided")
if not self.strict_direction:
self.avg_error_map = self.property_set["vf2_avg_error_map"]
if self.avg_error_map is None:
self.avg_error_map = vf2_utils.build_average_error_map(
self.target, self.properties, self.coupling_map
)
self.avg_error_map = vf2_utils.build_average_error_map(self.target, None)

result = vf2_utils.build_interaction_graph(dag, self.strict_direction)
if result is None:
Expand All @@ -172,67 +157,62 @@ def run(self, dag):
scoring_bit_list = vf2_utils.build_bit_list(im_graph, im_graph_node_map)
scoring_edge_list = vf2_utils.build_edge_list(im_graph)

if self.target is not None:
# If qargs is None then target is global and ideal so no
# scoring is needed
if self.target.qargs is None:
return
if self.strict_direction:
cm_graph = PyDiGraph(multigraph=False)
else:
cm_graph = PyGraph(multigraph=False)
# If None is present in qargs there are globally defined ideal operations
# we should add these to all entries based on the number of qubits, so we
# treat that as a valid operation even if there is no scoring for the
# strict direction case
global_ops = None
if None in self.target.qargs:
global_ops = {1: [], 2: []}
for op in self.target.operation_names_for_qargs(None):
operation = self.target.operation_for_name(op)
# If operation is a class this is a variable width ideal instruction
# so we treat it as available on both 1 and 2 qubits
if inspect.isclass(operation):
global_ops[1].append(op)
global_ops[2].append(op)
else:
num_qubits = operation.num_qubits
if num_qubits in global_ops:
global_ops[num_qubits].append(op)
op_names = []
for i in range(self.target.num_qubits):
try:
entry = set(self.target.operation_names_for_qargs((i,)))
except KeyError:
entry = set()
if global_ops is not None:
entry.update(global_ops[1])
op_names.append(entry)
cm_graph.add_nodes_from(op_names)
for qargs in self.target.qargs:
len_args = len(qargs)
# If qargs == 1 we already populated it and if qargs > 2 there are no instructions
# using those in the circuit because we'd have already returned by this point
if len_args == 2:
ops = set(self.target.operation_names_for_qargs(qargs))
if global_ops is not None:
ops.update(global_ops[2])
cm_graph.add_edge(qargs[0], qargs[1], ops)
cm_nodes = list(cm_graph.node_indexes())
# Filter qubits without any supported operations. If they
# don't support any operations, they're not valid for layout selection.
# This is only needed in the undirected case because in strict direction
# mode the node matcher will not match since none of the circuit ops
# will match the cmap ops.
if not self.strict_direction:
has_operations = set(itertools.chain.from_iterable(self.target.qargs))
to_remove = set(cm_graph.node_indices()).difference(has_operations)
if to_remove:
cm_graph.remove_nodes_from(list(to_remove))
# If qargs is None then target is global and ideal so no
# scoring is needed
if self.target.qargs is None:
return
if self.strict_direction:
cm_graph = PyDiGraph(multigraph=False)
else:
cm_graph, cm_nodes = vf2_utils.shuffle_coupling_graph(
self.coupling_map, self.seed, self.strict_direction
)
cm_graph = PyGraph(multigraph=False)
# If None is present in qargs there are globally defined ideal operations
# we should add these to all entries based on the number of qubits, so we
# treat that as a valid operation even if there is no scoring for the
# strict direction case
global_ops = None
if None in self.target.qargs:
global_ops = {1: [], 2: []}
for op in self.target.operation_names_for_qargs(None):
operation = self.target.operation_for_name(op)
# If operation is a class this is a variable width ideal instruction
# so we treat it as available on both 1 and 2 qubits
if inspect.isclass(operation):
global_ops[1].append(op)
global_ops[2].append(op)
else:
num_qubits = operation.num_qubits
if num_qubits in global_ops:
global_ops[num_qubits].append(op)
op_names = []
for i in range(self.target.num_qubits):
try:
entry = set(self.target.operation_names_for_qargs((i,)))
except KeyError:
entry = set()
if global_ops is not None:
entry.update(global_ops[1])
op_names.append(entry)
cm_graph.add_nodes_from(op_names)
for qargs in self.target.qargs:
len_args = len(qargs)
# If qargs == 1 we already populated it and if qargs > 2 there are no instructions
# using those in the circuit because we'd have already returned by this point
if len_args == 2:
ops = set(self.target.operation_names_for_qargs(qargs))
if global_ops is not None:
ops.update(global_ops[2])
cm_graph.add_edge(qargs[0], qargs[1], ops)
cm_nodes = list(cm_graph.node_indexes())
# Filter qubits without any supported operations. If they
# don't support any operations, they're not valid for layout selection.
# This is only needed in the undirected case because in strict direction
# mode the node matcher will not match since none of the circuit ops
# will match the cmap ops.
if not self.strict_direction:
has_operations = set(itertools.chain.from_iterable(self.target.qargs))
to_remove = set(cm_graph.node_indices()).difference(has_operations)
if to_remove:
cm_graph.remove_nodes_from(list(to_remove))

logger.debug("Running VF2 to find post transpile mappings")
if self.target and self.strict_direction:
Expand Down
Loading

0 comments on commit d9b8a18

Please sign in to comment.