Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve error messages on failed control-flow transpilation #9049

Merged
merged 3 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -168,6 +168,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 @@ -292,6 +292,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
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 @@ -1373,3 +1373,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)