Skip to content

Commit

Permalink
Improve error messages on failed control-flow transpilation (#9049)
Browse files Browse the repository at this point in the history
* Improve error messages on failed control-flow transpilation

As part of this, it became convenient for the `Error` transpiler pass to
be able to accept an arbitrary `property_set -> str` callable function
to generate the message, rather than just relying on fixed formatting
strings.

* Fix lint
  • Loading branch information
jakelishman authored Apr 17, 2023
1 parent 212b1b5 commit f51a93f
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 10 deletions.
18 changes: 13 additions & 5 deletions qiskit/transpiler/passes/utils/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ def __init__(self, msg=None, action="raise"):
"""Error pass.
Args:
msg (str): Error message, if not provided a generic error will be used
msg (str | Callable[[PropertySet], str]): Error message, if not provided a generic error
will be used. This can be either a raw string, or a callback function that accepts
the current ``property_set`` and returns the desired message.
action (str): the action to perform. Default: 'raise'. The options are:
* 'raise': Raises a `TranspilerError` exception with msg
* 'warn': Raises a non-fatal warning with msg
Expand All @@ -45,10 +47,16 @@ def __init__(self, msg=None, action="raise"):

def run(self, _):
"""Run the Error pass on `dag`."""
msg = self.msg if self.msg else "An error occurred while the passmanager was running."
prop_names = [tup[1] for tup in string.Formatter().parse(msg) if tup[1] is not None]
properties = {prop_name: self.property_set[prop_name] for prop_name in prop_names}
msg = msg.format(**properties)
if self.msg is None:
msg = "An error occurred while the pass manager was running."
elif isinstance(self.msg, str):
prop_names = [
tup[1] for tup in string.Formatter().parse(self.msg) if tup[1] is not None
]
properties = {prop_name: self.property_set[prop_name] for prop_name in prop_names}
msg = self.msg.format(**properties)
else:
msg = self.msg(self.property_set)

if self.action == "raise":
raise TranspilerError(msg)
Expand Down
35 changes: 31 additions & 4 deletions qiskit/transpiler/preset_passmanagers/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,39 @@ def _without_control_flow(property_set):
return not any(property_set[f"contains_{x}"] for x in _CONTROL_FLOW_OP_NAMES)


class _InvalidControlFlowForBackend:
# Explicitly stateful closure to allow pickling.

def __init__(self, basis_gates=(), target=None):
if target is not None:
self.unsupported = [op for op in _CONTROL_FLOW_OP_NAMES if op not in target]
else:
basis_gates = set(basis_gates) if basis_gates is not None else set()
self.unsupported = [op for op in _CONTROL_FLOW_OP_NAMES if op not in basis_gates]

def message(self, property_set):
"""Create an error message for the given property set."""
fails = [x for x in self.unsupported if property_set[f"contains_{x}"]]
if len(fails) == 1:
return f"The control-flow construct '{fails[0]}' is not supported by the backend."
return (
f"The control-flow constructs [{', '.join(repr(op) for op in fails)}]"
" are not supported by the backend."
)

def condition(self, property_set):
"""Checkable condition for the given property set."""
return any(property_set[f"contains_{x}"] for x in self.unsupported)


def generate_control_flow_options_check(
layout_method=None,
routing_method=None,
translation_method=None,
optimization_method=None,
scheduling_method=None,
basis_gates=(),
target=None,
):
"""Generate a pass manager that, when run on a DAG that contains control flow, fails with an
error message explaining the invalid options, and what could be used instead.
Expand All @@ -99,7 +126,6 @@ def generate_control_flow_options_check(
control-flow operations, and raises an error if any of the given options do not support
control flow, but a circuit with control flow is given.
"""

bad_options = []
message = "Some options cannot be used with control flow."
for stage, given in [
Expand All @@ -123,9 +149,10 @@ def generate_control_flow_options_check(
bad_options.append(option)
out = PassManager()
out.append(ContainsInstruction(_CONTROL_FLOW_OP_NAMES, recurse=False))
if not bad_options:
return out
out.append(Error(message), condition=_has_control_flow)
if bad_options:
out.append(Error(message), condition=_has_control_flow)
backend_control = _InvalidControlFlowForBackend(basis_gates, target)
out.append(Error(backend_control.message), condition=backend_control.condition)
return out


Expand Down
2 changes: 2 additions & 0 deletions qiskit/transpiler/preset_passmanagers/level0.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ def _swap_mapped(property_set):
translation_method=translation_method,
optimization_method=optimization_method,
scheduling_method=scheduling_method,
basis_gates=basis_gates,
target=target,
)
if init_method is not None:
init += plugin_manager.get_passmanager_stage(
Expand Down
2 changes: 2 additions & 0 deletions qiskit/transpiler/preset_passmanagers/level1.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ def _unroll_condition(property_set):
translation_method=translation_method,
optimization_method=optimization_method,
scheduling_method=scheduling_method,
basis_gates=basis_gates,
target=target,
)
if init_method is not None:
init += plugin_manager.get_passmanager_stage(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
features:
- |
The transpiler pass :class:`~.transpiler.passes.Error` now accepts callables
in its ``msg`` parameter. These should be a callable that takes in the
``property_set`` and returns a string.
12 changes: 12 additions & 0 deletions test/python/transpiler/test_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ def test_logger(self):
pass_.run(None)
self.assertEqual(log.output, ["INFO:qiskit.transpiler.passes.utils.error:a message"])

def test_message_callable(self):
"""Test that the message can be a callable that accepts the property set."""

def message(property_set):
self.assertIn("sentinel key", property_set)
return property_set["sentinel key"]

pass_ = Error(message)
pass_.property_set["sentinel key"] = "sentinel value"
with self.assertRaisesRegex(TranspilerError, "sentinel value"):
pass_.run(None)


if __name__ == "__main__":
unittest.main()
55 changes: 54 additions & 1 deletion test/python/transpiler/test_preset_passmanagers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister
from qiskit.circuit import Qubit, Gate, ControlFlowOp, ForLoopOp
from qiskit.compiler import transpile, assemble
from qiskit.transpiler import CouplingMap, Layout, PassManager, TranspilerError
from qiskit.transpiler import CouplingMap, Layout, PassManager, TranspilerError, Target
from qiskit.circuit.library import U2Gate, U3Gate, QuantumVolume, CXGate, CZGate, XGate
from qiskit.transpiler.passes import (
ALAPScheduleAnalysis,
Expand Down Expand Up @@ -1561,3 +1561,56 @@ def test_unsupported_levels_raise(self, optimization_level):

with self.assertRaisesRegex(TranspilerError, "The optimizations in optimization_level="):
transpile(qc, optimization_level=optimization_level)

@data(0, 1)
def test_unsupported_basis_gates_raise(self, optimization_level):
"""Test that trying to transpile a control-flow circuit for a backend that doesn't support
the necessary operations in its `basis_gates` will raise a sensible error."""
backend = FakeTokyo()

qc = QuantumCircuit(1, 1)
with qc.for_loop((0,)):
pass
with self.assertRaisesRegex(TranspilerError, "The control-flow construct.*not supported"):
transpile(qc, backend, optimization_level=optimization_level)

qc = QuantumCircuit(1, 1)
with qc.if_test((qc.clbits[0], False)):
pass
with self.assertRaisesRegex(TranspilerError, "The control-flow construct.*not supported"):
transpile(qc, backend, optimization_level=optimization_level)

qc = QuantumCircuit(1, 1)
with qc.while_loop((qc.clbits[0], False)):
pass
with qc.for_loop((0, 1, 2)):
pass
with self.assertRaisesRegex(TranspilerError, "The control-flow construct.*not supported"):
transpile(qc, backend, optimization_level=optimization_level)

@data(0, 1)
def test_unsupported_targets_raise(self, optimization_level):
"""Test that trying to transpile a control-flow circuit for a backend that doesn't support
the necessary operations in its `Target` will raise a more sensible error."""
target = Target(num_qubits=2)
target.add_instruction(CXGate(), {(0, 1): None})

qc = QuantumCircuit(1, 1)
with qc.for_loop((0,)):
pass
with self.assertRaisesRegex(TranspilerError, "The control-flow construct.*not supported"):
transpile(qc, target=target, optimization_level=optimization_level)

qc = QuantumCircuit(1, 1)
with qc.if_test((qc.clbits[0], False)):
pass
with self.assertRaisesRegex(TranspilerError, "The control-flow construct.*not supported"):
transpile(qc, target=target, optimization_level=optimization_level)

qc = QuantumCircuit(1, 1)
with qc.while_loop((qc.clbits[0], False)):
pass
with qc.for_loop((0, 1, 2)):
pass
with self.assertRaisesRegex(TranspilerError, "The control-flow construct.*not supported"):
transpile(qc, target=target, optimization_level=optimization_level)

0 comments on commit f51a93f

Please sign in to comment.