Skip to content

Commit

Permalink
Filter/ignore qubits in Target without any operations (Qiskit#9927)
Browse files Browse the repository at this point in the history
* Filter/ignore qubits in Target without any operations

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.

* Expand testing

* Make filtered qubit coupling map a Target.build_coupling_map option

This commit reworks the logic to construct a filtered coupling map as an
optional argument on `Target.build_coupling_map()`. This makes the
filtering a function of the Target object itself, which is where the
context/data about which qubits support operations or not lives. The
previous versions of this PR had a weird mix of responsibilities where
the target would generate a coupling map and then we'd pass the target
to that coupling map to do an additional round of filtering on it.

* Apply suggestions from code review

Co-authored-by: John Lapeyre <[email protected]>

* Fix incorrect set construction

* Expand docstring on build_coupling_map argument

* Rework logic in vf2 passes for filtering

* Update argument name in disjoint_utils.py

* Inline second argument for require_layout_isolated_to_component

Co-authored-by: Kevin Hartman <[email protected]>

* Update qiskit/transpiler/passes/layout/vf2_post_layout.py

Co-authored-by: Kevin Hartman <[email protected]>

* Apply suggestions from code review

Co-authored-by: Kevin Hartman <[email protected]>

* Remove unnecessary len()

* Inline second arg for dense_layout too

---------

Co-authored-by: John Lapeyre <[email protected]>
Co-authored-by: Kevin Hartman <[email protected]>
  • Loading branch information
3 people authored and king-p3nguin committed May 22, 2023
1 parent aa9e083 commit 2d27c8c
Show file tree
Hide file tree
Showing 14 changed files with 185 additions and 15 deletions.
4 changes: 3 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,9 @@ 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 if self.target is None else self.target,
self._inner_run,
)
layout_mapping = {}
for component in layout_components:
Expand Down
22 changes: 19 additions & 3 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, Union
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 @@ -30,13 +31,22 @@

def run_pass_over_connected_components(
dag: DAGCircuit,
coupling_map: CouplingMap,
components_source: Union[Target, CouplingMap],
run_func: Callable[[DAGCircuit, CouplingMap], T],
) -> List[T]:
"""Run a transpiler pass inner function over mapped components."""
if isinstance(components_source, Target):
coupling_map = components_source.build_coupling_map(filter_idle_qubits=True)
else:
coupling_map = components_source
cmap_components = coupling_map.connected_components()
# 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,9 +137,15 @@ def combine_barriers(dag: DAGCircuit, retain_uuid: bool = True):
node.op.label = None


def require_layout_isolated_to_component(dag: DAGCircuit, coupling_map: CouplingMap) -> bool:
def require_layout_isolated_to_component(
dag: DAGCircuit, components_source: Union[Target, CouplingMap]
) -> bool:
"""Check that the layout of the dag does not require connectivity across connected components
in the CouplingMap"""
if isinstance(components_source, Target):
coupling_map = components_source.build_coupling_map(filter_idle_qubits=True)
else:
coupling_map = components_source
qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)}
component_sets = [set(x.graph.nodes()) for x in coupling_map.connected_components()]
for inst in dag.two_qubit_ops():
Expand Down
12 changes: 11 additions & 1 deletion qiskit/transpiler/passes/layout/sabre_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,18 @@ def run(self, dag):
self.routing_pass.fake_run = False
return dag
# Combined
if self.target is not None:
# This is a special case SABRE only works with a bidirectional coupling graph
# which we explicitly can't create from the target. So do this manually here
# to avoid altering the shared state with the unfiltered indices.
target = self.target.build_coupling_map(filter_idle_qubits=True)
target.make_symmetric()
else:
target = self.coupling_map
layout_components = disjoint_utils.run_pass_over_connected_components(
dag, self.coupling_map, self._inner_run
dag,
target,
self._inner_run,
)
initial_layout_dict = {}
final_layout_dict = {}
Expand Down
13 changes: 13 additions & 0 deletions qiskit/transpiler/passes/layout/vf2_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""VF2Layout pass to find a layout using subgraph isomorphism"""
import os
from enum import Enum
import itertools
import logging
import time

Expand Down Expand Up @@ -141,6 +142,14 @@ 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:
has_operations = set(itertools.chain.from_iterable(self.target.qargs))
to_remove = set(range(len(cm_nodes))).difference(has_operations)
if to_remove:
cm_graph.remove_nodes_from([cm_nodes[i] for i in to_remove])

# 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 +248,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
11 changes: 11 additions & 0 deletions qiskit/transpiler/passes/layout/vf2_post_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from enum import Enum
import logging
import inspect
import itertools
import time

from rustworkx import PyDiGraph, vf2_mapping, PyGraph
Expand Down Expand Up @@ -215,6 +216,16 @@ 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:
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))
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
5 changes: 3 additions & 2 deletions qiskit/transpiler/passes/routing/basic_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +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.require_layout_isolated_to_component(dag, self.coupling_map)

disjoint_utils.require_layout_isolated_to_component(
dag, self.coupling_map if self.target is None else self.target
)
canonical_register = dag.qregs["q"]
trivial_layout = Layout.generate_trivial_layout(canonical_register)
current_layout = trivial_layout.copy()
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.require_layout_isolated_to_component(dag, self.coupling_map)
disjoint_utils.require_layout_isolated_to_component(
dag, self.coupling_map if self.target is None else 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.require_layout_isolated_to_component(dag, self.coupling_map)
disjoint_utils.require_layout_isolated_to_component(
dag, self.coupling_map if self.target is None else 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.require_layout_isolated_to_component(dag, self.coupling_map)
disjoint_utils.require_layout_isolated_to_component(
dag, self.coupling_map if self.target is None else self.target
)

self.dist_matrix = self.coupling_map.distance_matrix

Expand Down
5 changes: 3 additions & 2 deletions qiskit/transpiler/passes/routing/stochastic_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,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.require_layout_isolated_to_component(dag, self.coupling_map)
disjoint_utils.require_layout_isolated_to_component(
dag, self.coupling_map if self.target is None else self.target
)

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

Expand Down
26 changes: 23 additions & 3 deletions qiskit/transpiler/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from __future__ import annotations


import itertools
import warnings

from typing import Tuple, Union, Optional, Dict, List, Any
Expand Down Expand Up @@ -996,7 +996,7 @@ def _build_coupling_graph(self):
if self._coupling_graph.num_edges() == 0 and any(x is None for x in self._qarg_gate_map):
self._coupling_graph = None

def build_coupling_map(self, two_q_gate=None):
def build_coupling_map(self, two_q_gate=None, filter_idle_qubits=False):
"""Get a :class:`~qiskit.transpiler.CouplingMap` from this target.
If there is a mix of two qubit operations that have a connectivity
Expand All @@ -1011,6 +1011,14 @@ def build_coupling_map(self, two_q_gate=None):
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.
filter_idle_qubits (bool): If set to ``True`` the output :class:`~.CouplingMap`
will remove any qubits that don't have any operations defined in the
target. Note that using this argument will result in an output
:class:`~.CouplingMap` object which has holes in its indices
which might differ from the assumptions of the class. The typical use
case of this argument is to be paired with with
:meth:`.CouplingMap.connected_components` which will handle the holes
as expected.
Returns:
CouplingMap: The :class:`~qiskit.transpiler.CouplingMap` object
for this target. If there are no connectivity constraints in
Expand Down Expand Up @@ -1048,11 +1056,23 @@ def build_coupling_map(self, two_q_gate=None):
# existing and return
if self._coupling_graph is not None:
cmap = CouplingMap()
cmap.graph = self._coupling_graph
if filter_idle_qubits:
cmap.graph = self._filter_coupling_graph()
else:
cmap.graph = self._coupling_graph
return cmap
else:
return None

def _filter_coupling_graph(self):
has_operations = set(itertools.chain.from_iterable(self.qargs))
graph = self._coupling_graph
to_remove = set(graph.node_indices()).difference(has_operations)
if to_remove:
graph = graph.copy()
graph.remove_nodes_from(list(to_remove))
return graph

@property
def physical_qubits(self):
"""Returns a sorted list of physical_qubits"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
features:
- |
The :meth:`~.Target.build_coupling_map` method has a new keyword argument,
``filter_idle_qubits`` which when set to ``True`` will remove any qubits
from the output :class:`~.CouplingMap` that don't support any operations.
82 changes: 82 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 @@ -2733,3 +2734,84 @@ 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}
self.assertEqual(tqc.num_qubits, 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_with_routing(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(4)})
target.add_instruction(HGate(), {(i,): InstructionProperties(error=0.5) for i in range(4)})
target.add_instruction(
CXGate(),
{edge: InstructionProperties(error=0.5) for edge in [(0, 1), (1, 2), (2, 0), (2, 3)]},
)
qc = QuantumCircuit(4)
qc.x(0)
qc.cx(0, 1)
qc.cx(0, 2)
qc.cx(1, 3)
qc.cx(0, 3)
tqc = transpile(qc, target=target, optimization_level=opt_level)
invalid_qubits = {
4,
}
self.assertEqual(tqc.num_qubits, 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(0, 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 2d27c8c

Please sign in to comment.