diff --git a/qiskit/transpiler/passes/utils/error.py b/qiskit/transpiler/passes/utils/error.py index 475456b86d12..445b1d21d565 100644 --- a/qiskit/transpiler/passes/utils/error.py +++ b/qiskit/transpiler/passes/utils/error.py @@ -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 @@ -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) diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 4d15fca99f24..2b696553a507 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -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. @@ -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 [ @@ -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 diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index d137e16bbe88..c073a4ad83c8 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -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( diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 1ad77652005e..26624d02ea1d 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -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( diff --git a/releasenotes/notes/error-pass-callable-message-3f29f09b9faba736.yaml b/releasenotes/notes/error-pass-callable-message-3f29f09b9faba736.yaml new file mode 100644 index 000000000000..d307864cb2ec --- /dev/null +++ b/releasenotes/notes/error-pass-callable-message-3f29f09b9faba736.yaml @@ -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. diff --git a/test/python/transpiler/test_error.py b/test/python/transpiler/test_error.py index c541283d0fb0..e110eaaa4180 100644 --- a/test/python/transpiler/test_error.py +++ b/test/python/transpiler/test_error.py @@ -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() diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 019750c76fc6..55d626c097c9 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -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, @@ -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)