Skip to content

Commit

Permalink
Filter/ignore qubits in Target without any operations
Browse files Browse the repository at this point in the history
Building off Qiskit#9840 which adds full path support in all the preset pass
managers for targetting backends with a disconnected coupling graph,
this commit adds support for ignoring qubits that do not support any
operations. When a Target is generated from Qiskit#9911 with `filter_faulty`
set to `True` this will potentially result in qubits being present in
the `Target` without any supported operations. In these cases the
layout passes in the transpiler might inadvertently use these qubits
only to fail in the basis translator because there are no instructions
available. This commit adds filtering of connected components from the
list of output connected components if the `Target` does have any
supported instructions on a qubit.

This works by building a copy of the coupling map's internal graph
that removes the nodes which do not have any supported operations.
Then when we compute the connected components of this graph it will
exclude any components of isolated qubits without any operations
supported. A similar change is made to the coupling graph we pass to
rustworkx.vf2_mapping() inside the vf2 layout family of passes.
  • Loading branch information
mtreinish committed Apr 13, 2023
1 parent 37fc39c commit f537090
Show file tree
Hide file tree
Showing 13 changed files with 142 additions and 16 deletions.
28 changes: 23 additions & 5 deletions qiskit/transpiler/coupling.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ def largest_connected_component(self):
"""Return a set of qubits in the largest connected component."""
return max(rx.weakly_connected_components(self.graph), key=len)

def connected_components(self) -> List["CouplingMap"]:
def connected_components(self, target=None) -> List["CouplingMap"]:
"""Separate a :Class:`~.CouplingMap` into subgraph :class:`~.CouplingMap`
for each connected component.
Expand Down Expand Up @@ -463,20 +463,38 @@ def connected_components(self) -> List["CouplingMap"]:
will print ``3`` as index ``0`` in the second component is qubit 3 in the original cmap.
Args:
target (Target): If used the provided :class:`~.Target` will be used to filter any
qubits that do not have any supported operations so that those qubits
will not be included as a connected component in the output list. When not
specified these qubits without any supported operations will be included in
the output as a connected component of a single qubit.
Returns:
list: A list of :class:`~.CouplingMap` objects for each connected
components. The order of this list is deterministic but
implementation specific and shouldn't be relied upon as
part of the API.
"""
graph = self.graph
if target is not None:
qargs = target.qargs
graph = self.graph.copy()
for index in graph.node_indices():
for qargs in target.qargs:
if index in qargs:
break
else:
graph.remove_node(index)

# Set payload to index
for node in self.graph.node_indices():
self.graph[node] = node
components = rx.weakly_connected_components(self.graph)
for node in graph.node_indices():
graph[node] = node
components = rx.weakly_connected_components(graph)
output_list = []
for component in components:
new_cmap = CouplingMap()
new_cmap.graph = self.graph.subgraph(list(sorted(component)))
new_cmap.graph = graph.subgraph(list(sorted(component)))
output_list.append(new_cmap)
return output_list

Expand Down
5 changes: 4 additions & 1 deletion qiskit/transpiler/passes/layout/dense_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ def run(self, dag):
"A coupling_map or target with constrained qargs is necessary to run the pass."
)
layout_components = disjoint_utils.run_pass_over_connected_components(
dag, self.coupling_map, self._inner_run
dag,
self.coupling_map,
self._inner_run,
self.target,
)
layout_mapping = {}
for component in layout_components:
Expand Down
19 changes: 15 additions & 4 deletions qiskit/transpiler/passes/layout/disjoint_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"""This module contains common utils for disjoint coupling maps."""

from collections import defaultdict
from typing import List, Callable, TypeVar, Dict
from typing import List, Callable, TypeVar, Dict, Optional
import uuid

import rustworkx as rx
Expand All @@ -22,6 +22,7 @@
from qiskit.dagcircuit.dagcircuit import DAGCircuit
from qiskit.dagcircuit.dagnode import DAGOutNode
from qiskit.transpiler.coupling import CouplingMap
from qiskit.transpiler.target import Target
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.passes.layout import vf2_utils

Expand All @@ -32,11 +33,17 @@ def run_pass_over_connected_components(
dag: DAGCircuit,
coupling_map: CouplingMap,
run_func: Callable[[DAGCircuit, CouplingMap], T],
target: Optional[Target] = None,
) -> List[T]:
"""Run a transpiler pass inner function over mapped components."""
cmap_components = coupling_map.connected_components()
cmap_components = coupling_map.connected_components(target=target)
# If graph is connected we only need to run the pass once
if len(cmap_components) == 1:
if dag.num_qubits() > cmap_components[0].size():
raise TranspilerError(
"A connected component of the DAGCircuit is too large for any of the connected "
"components in the coupling map."
)
return [run_func(dag, cmap_components[0])]
dag_components = separate_dag(dag)
mapped_components = map_components(dag_components, cmap_components)
Expand Down Expand Up @@ -127,11 +134,15 @@ def combine_barriers(dag: DAGCircuit, retain_uuid: bool = True):
node.op.label = None


def check_layout_isolated_to_component(dag: DAGCircuit, coupling_map: CouplingMap) -> bool:
def check_layout_isolated_to_component(
dag: DAGCircuit, coupling_map: CouplingMap, target: Optional[Target] = None
) -> bool:
"""Check that the layout of the dag does not require connectivity across connected components
in the CouplingMap"""
qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)}
component_sets = [set(x.graph.nodes()) for x in coupling_map.connected_components()]
component_sets = [
set(x.graph.nodes()) for x in coupling_map.connected_components(target=target)
]
for inst in dag.two_qubit_ops():
component_index = None
for i in range(len(component_sets)):
Expand Down
5 changes: 4 additions & 1 deletion qiskit/transpiler/passes/layout/sabre_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,10 @@ def run(self, dag):
return dag
# Combined
layout_components = disjoint_utils.run_pass_over_connected_components(
dag, self.coupling_map, self._inner_run
dag,
self.coupling_map,
self._inner_run,
target=self.target,
)
initial_layout_dict = {}
final_layout_dict = {}
Expand Down
14 changes: 14 additions & 0 deletions qiskit/transpiler/passes/layout/vf2_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,16 @@ def run(self, dag):
cm_graph, cm_nodes = vf2_utils.shuffle_coupling_graph(
self.coupling_map, self.seed, self.strict_direction
)
# Filter qubits without any supported operations. If they don't support any operations
# They're not valid for layout selection
if self.target is not None:
for qubit, graph_index in enumerate(cm_nodes):
for qargs in self.target.qargs:
if qubit in qargs:
break
else:
cm_graph.remove_node(graph_index)

# To avoid trying to over optimize the result by default limit the number
# of trials based on the size of the graphs. For circuits with simple layouts
# like an all 1q circuit we don't want to sit forever trying every possible
Expand Down Expand Up @@ -239,6 +249,10 @@ def mapping_to_layout(layout_mapping):
reverse_im_graph_node_map,
self.avg_error_map,
)
# No free qubits for free qubit mapping
if chosen_layout is None:
self.property_set["VF2Layout_stop_reason"] = VF2LayoutStopReason.NO_SOLUTION_FOUND
return
self.property_set["layout"] = chosen_layout
for reg in dag.qregs.values():
self.property_set["layout"].add_register(reg)
Expand Down
9 changes: 9 additions & 0 deletions qiskit/transpiler/passes/layout/vf2_post_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ def run(self, dag):
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:
for node_index in cm_graph.node_indices():
if not cm_graph[node_index]:
cm_graph.remove_node(node_index)
else:
cm_graph, cm_nodes = vf2_utils.shuffle_coupling_graph(
self.coupling_map, self.seed, self.strict_direction
Expand Down
2 changes: 2 additions & 0 deletions qiskit/transpiler/passes/layout/vf2_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ def map_free_qubits(
set(range(num_physical_qubits)) - partial_layout.get_physical_bits().keys()
)
for im_index in sorted(free_nodes, key=lambda x: sum(free_nodes[x].values())):
if not free_qubits:
return None
selected_qubit = free_qubits.pop(0)
partial_layout.add(reverse_bit_map[im_index], selected_qubit)
return partial_layout
4 changes: 3 additions & 1 deletion qiskit/transpiler/passes/routing/basic_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ def run(self, dag):

if len(dag.qubits) > len(self.coupling_map.physical_qubits):
raise TranspilerError("The layout does not match the amount of qubits in the DAG")
disjoint_utils.check_layout_isolated_to_component(dag, self.coupling_map)
disjoint_utils.check_layout_isolated_to_component(
dag, self.coupling_map, target=self.target
)

canonical_register = dag.qregs["q"]
trivial_layout = Layout.generate_trivial_layout(canonical_register)
Expand Down
4 changes: 3 additions & 1 deletion qiskit/transpiler/passes/routing/bip_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ def run(self, dag):
"BIPMapping requires the number of virtual and physical qubits to be the same. "
"Supply 'qubit_subset' to specify physical qubits to use."
)
disjoint_utils.check_layout_isolated_to_component(dag, self.coupling_map)
disjoint_utils.check_layout_isolated_to_component(
dag, self.coupling_map, target=self.target
)

original_dag = dag

Expand Down
4 changes: 3 additions & 1 deletion qiskit/transpiler/passes/routing/lookahead_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ def run(self, dag):
f"The number of DAG qubits ({len(dag.qubits)}) is greater than the number of "
f"available device qubits ({number_of_available_qubits})."
)
disjoint_utils.check_layout_isolated_to_component(dag, self.coupling_map)
disjoint_utils.check_layout_isolated_to_component(
dag, self.coupling_map, target=self.target
)

register = dag.qregs["q"]
current_state = _SystemState(
Expand Down
4 changes: 3 additions & 1 deletion qiskit/transpiler/passes/routing/sabre_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,9 @@ def run(self, dag):
heuristic = Heuristic.Decay
else:
raise TranspilerError("Heuristic %s not recognized." % self.heuristic)
disjoint_utils.check_layout_isolated_to_component(dag, self.coupling_map)
disjoint_utils.check_layout_isolated_to_component(
dag, self.coupling_map, target=self.target
)

self.dist_matrix = self.coupling_map.distance_matrix

Expand Down
4 changes: 3 additions & 1 deletion qiskit/transpiler/passes/routing/stochastic_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ def run(self, dag):
if len(dag.qubits) > len(self.coupling_map.physical_qubits):
raise TranspilerError("The layout does not match the amount of qubits in the DAG")

disjoint_utils.check_layout_isolated_to_component(dag, self.coupling_map)
disjoint_utils.check_layout_isolated_to_component(
dag, self.coupling_map, target=self.target
)

self.rng = np.random.default_rng(self.seed)

Expand Down
56 changes: 56 additions & 0 deletions test/python/compiler/test_transpiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
CZGate,
XGate,
SXGate,
HGate,
)
from qiskit.circuit import IfElseOp, WhileLoopOp, ForLoopOp, ControlFlowOp
from qiskit.circuit.measure import Measure
Expand Down Expand Up @@ -2731,3 +2732,58 @@ def test_six_component_circuit_dense_layout(self, routing_method):
if op_name == "barrier":
continue
self.assertIn(qubits, self.backend.target[op_name])

@data(0, 1, 2, 3)
def test_transpile_target_with_qubits_without_ops(self, opt_level):
"""Test qubits without operations aren't ever used."""
target = Target(num_qubits=5)
target.add_instruction(XGate(), {(i,): InstructionProperties(error=0.5) for i in range(3)})
target.add_instruction(HGate(), {(i,): InstructionProperties(error=0.5) for i in range(3)})
target.add_instruction(
CXGate(), {edge: InstructionProperties(error=0.5) for edge in [(0, 1), (1, 2), (2, 0)]}
)
qc = QuantumCircuit(3)
qc.x(0)
qc.cx(0, 1)
qc.cx(0, 2)
tqc = transpile(qc, target=target, optimization_level=opt_level)
invalid_qubits = {3, 4, 5}
for inst in tqc.data:
for bit in inst.qubits:
self.assertNotIn(tqc.find_bit(bit).index, invalid_qubits)

@data(0, 1, 2, 3)
def test_transpile_target_with_qubits_without_ops_circuit_too_large(self, opt_level):
"""Test qubits without operations aren't ever used and error if circuit needs them."""
target = Target(num_qubits=5)
target.add_instruction(XGate(), {(i,): InstructionProperties(error=0.5) for i in range(3)})
target.add_instruction(HGate(), {(i,): InstructionProperties(error=0.5) for i in range(3)})
target.add_instruction(
CXGate(), {edge: InstructionProperties(error=0.5) for edge in [(0, 1), (1, 2), (2, 0)]}
)
qc = QuantumCircuit(4)
qc.x(0)
qc.cx(0, 1)
qc.cx(0, 2)
qc.cx(0, 3)
with self.assertRaises(TranspilerError):
transpile(qc, target=target, optimization_level=opt_level)

@data(1, 2, 3)
def test_transpile_target_with_qubits_without_ops_circuit_too_large_disconnected(
self, opt_level
):
"""Test qubits without operations aren't ever used if a disconnected circuit needs them."""
target = Target(num_qubits=5)
target.add_instruction(XGate(), {(i,): InstructionProperties(error=0.5) for i in range(3)})
target.add_instruction(HGate(), {(i,): InstructionProperties(error=0.5) for i in range(3)})
target.add_instruction(
CXGate(), {edge: InstructionProperties(error=0.5) for edge in [(0, 1), (1, 2), (2, 0)]}
)
qc = QuantumCircuit(5)
qc.x(0)
qc.x(1)
qc.x(3)
qc.x(4)
with self.assertRaises(TranspilerError):
transpile(qc, target=target, optimization_level=opt_level)

0 comments on commit f537090

Please sign in to comment.