From 473350f51e4d2cc8a2b5689451786275d2ecf2e3 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 6 Jun 2025 18:14:58 +0100 Subject: [PATCH 1/2] Support simple `IfElseOp` conditionals in OpenQASM 2 export With the removal of `Instruction.c_if` in gh-13506, we lost the ability to export the sort of simple conditionals that OpenQASM 2 can represent. This restores the ability, for the subset of `IfElseOp` objects that OpenQASM 2 can represent. It is possible to make the exporter slightly more permissive in some cases, if desired/necessary. --- qiskit/qasm2/export.py | 66 ++++++--- .../notes/qasm2-if-285d10169acb3f8d.yaml | 6 + test/python/qasm2/test_export.py | 132 ++++++++++++++++++ 3 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 releasenotes/notes/qasm2-if-285d10169acb3f8d.yaml diff --git a/qiskit/qasm2/export.py b/qiskit/qasm2/export.py index 8379987aeb4e..75f820ccf695 100644 --- a/qiskit/qasm2/export.py +++ b/qiskit/qasm2/export.py @@ -33,6 +33,7 @@ Parameter, library as lib, ) +from qiskit.circuit.classical import expr from qiskit.circuit.tools import pi_check from .exceptions import QASM2ExportError @@ -151,11 +152,12 @@ def dumps(circuit: QuantumCircuit, /) -> str: if regless_clbits: dummy_registers.append(ClassicalRegister(name="cregless", bits=regless_clbits)) register_escaped_names: dict[str, QuantumRegister | ClassicalRegister] = {} + name_from_register: dict[QuantumRegister | ClassicalRegister, str] = {} for regs in (circuit.qregs, circuit.cregs, dummy_registers): for reg in regs: - register_escaped_names[ - _make_unique(_escape_name(reg.name, "reg_"), register_escaped_names) - ] = reg + name = _make_unique(_escape_name(reg.name, "reg_"), register_escaped_names) + register_escaped_names[name] = reg + name_from_register[reg] = name bit_labels: dict[Qubit | Clbit, str] = { bit: f"{name}[{idx}]" for name, register in register_escaped_names.items() @@ -165,25 +167,53 @@ def dumps(circuit: QuantumCircuit, /) -> str: f"{'qreg' if isinstance(reg, QuantumRegister) else 'creg'} {name}[{reg.size}];" for name, reg in register_escaped_names.items() ) - instruction_calls = [] - for instruction in circuit._data: - operation = instruction.operation - if operation.name == "measure": + + def qasm_line(instruction) -> str | None: + if instruction.is_control_flow(): + operation = instruction.operation + if operation.name != "if_else": + raise QASM2ExportError("OpenQASM 2 does not support control-flow constructs") + if isinstance(operation.condition, expr.Expr) or isinstance( + operation.condition[0], Clbit + ): + # This could be relaxed (somewhat) in the future by attempting to resolve the `Expr` + # into a simple "register-equality" form; `Expr` is a superset of allowed conditions + raise QASM2ExportError("OpenQASM 2 only supports register-equality conditions") + if len(operation.blocks) != 1: + raise QASM2ExportError("OpenQASM 2 does not support 'else' statements") + body = operation.blocks[0] + if len(body.data) != 1: + # This could be relaxed in the future by "unrolling" the block and putting + # individual `if` conditions on each instruction. + raise QASM2ExportError( + "OpenQASM 2 only supports conditionals on single instructions" + ) + inner_instruction = body.data[0] + if inner_instruction.name == "if_else": + raise QASM2ExportError("OpenQASM 2 does not support nested conditionals") + if inner_instruction.name == "barrier": + raise QASM2ExportError("barriers cannot be conditional in OpenQASM 2") + reg, value = operation.condition + return f"if ({name_from_register[reg]} == {int(value)}) {qasm_line(inner_instruction)}" + if instruction.name == "measure": qubit = instruction.qubits[0] clbit = instruction.clbits[0] - instruction_qasm = f"measure {bit_labels[qubit]} -> {bit_labels[clbit]};" - elif operation.name == "reset": - instruction_qasm = f"reset {bit_labels[instruction.qubits[0]]};" - elif operation.name == "barrier": + return f"measure {bit_labels[qubit]} -> {bit_labels[clbit]};" + if instruction.name == "reset": + return f"reset {bit_labels[instruction.qubits[0]]};" + if instruction.name == "barrier": if not instruction.qubits: - # Barriers with no operands are invalid in (strict) OQ2, and the statement - # would have no meaning anyway. - continue + # Barriers with no operands are invalid in (strict) OQ2. Qiskit doesn't track + # "global" barriers specially, so that case is handled by explicitly listing all the + # qubits already. + return None qargs = ",".join(bit_labels[q] for q in instruction.qubits) - instruction_qasm = "barrier;" if not qargs else f"barrier {qargs};" - else: - instruction_qasm = _custom_operation_statement(instruction, gates_to_define, bit_labels) - instruction_calls.append(instruction_qasm) + return f"barrier {qargs};" + return _custom_operation_statement(instruction, gates_to_define, bit_labels) + + instruction_calls = [ + line for instruction in circuit._data if (line := qasm_line(instruction)) is not None + ] instructions_qasm = "\n".join(f"{call}" for call in instruction_calls) gate_definitions_qasm = "\n".join(f"{qasm}" for _, qasm in gates_to_define.values()) diff --git a/releasenotes/notes/qasm2-if-285d10169acb3f8d.yaml b/releasenotes/notes/qasm2-if-285d10169acb3f8d.yaml new file mode 100644 index 000000000000..367f1b1fb6bd --- /dev/null +++ b/releasenotes/notes/qasm2-if-285d10169acb3f8d.yaml @@ -0,0 +1,6 @@ +--- +features_qasm: + - | + The OpenQASM 2 exporter (:func:`.qasm2.dumps`) now supports outputting simple single-instruction + :class:`.IfElse` blocks, if the condition is a simple register–integer equality test. This + corresponds to what the OpenQASM 2 language can represent. diff --git a/test/python/qasm2/test_export.py b/test/python/qasm2/test_export.py index 2253fea03395..978606f60caf 100644 --- a/test/python/qasm2/test_export.py +++ b/test/python/qasm2/test_export.py @@ -22,6 +22,7 @@ from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, qasm2 from qiskit.circuit import Parameter, Qubit, Clbit, Gate, library as lib +from qiskit.circuit.classical import expr from test import QiskitTestCase # pylint: disable=wrong-import-order # Regex pattern to match valid OpenQASM identifiers @@ -44,6 +45,12 @@ def test_basic_output(self): qc.barrier(qr2) qc.cx(qr2[1], qr1[0]) qc.h(qr2[1]) + with qc.if_test((cr, 0)): + qc.x(qr2[1]) + with qc.if_test((cr, 1)): + qc.y(qr1[0]) + with qc.if_test((cr, 2)): + qc.z(qr1[0]) qc.barrier(qr1, qr2) qc.measure(qr1[0], cr[0]) qc.measure(qr2[0], cr[1]) @@ -62,6 +69,9 @@ def test_basic_output(self): barrier qr2[0],qr2[1]; cx qr2[1],qr1[0]; h qr2[1]; +if (cr == 0) x qr2[1]; +if (cr == 1) y qr1[0]; +if (cr == 2) z qr1[0]; barrier qr1[0],qr2[0],qr2[1]; measure qr1[0] -> cr[0]; measure qr2[0] -> cr[1]; @@ -854,6 +864,128 @@ def test_can_write_to_stream(self): written = stream.getvalue() self.assertEqual(qasm2.loads(written), qc) + def test_measure_can_be_conditioned(self): + qr = QuantumRegister(1, "qr") + cr = ClassicalRegister(1, "cr") + qc = QuantumCircuit(qr, cr) + with qc.if_test((cr, 0)): + qc.measure(0, 0) + expected = """\ +OPENQASM 2.0; +include "qelib1.inc"; +qreg qr[1]; +creg cr[1]; +if (cr == 0) measure qr[0] -> cr[0];""" + self.assertEqual(qasm2.dumps(qc), expected) + + def test_reset_can_be_conditioned(self): + qr = QuantumRegister(1, "qr") + cr = ClassicalRegister(1, "cr") + qc = QuantumCircuit(qr, cr) + with qc.if_test((cr, 0)): + qc.reset(0) + expected = """\ +OPENQASM 2.0; +include "qelib1.inc"; +qreg qr[1]; +creg cr[1]; +if (cr == 0) reset qr[0];""" + self.assertEqual(qasm2.dumps(qc), expected) + + def test_escaped_register_name_works_in_conditional(self): + qr = QuantumRegister(1, "qr") + # This is an invalid register name in OpenQASM 2 because it collides with a keyword. + cr = ClassicalRegister(1, "measure") + qc = QuantumCircuit(qr, cr) + with qc.if_test((cr, 0)): + qc.x(0) + expected = r"""OPENQASM 2\.0; +include "qelib1\.inc"; +qreg qr\[1\]; +creg (\w+)\[1\]; +if \(\1 == 0\) x qr\[0\];""" + match = re.fullmatch(expected, qasm2.dumps(qc), flags=re.MULTILINE) + self.assertIsNotNone(match) + self.assertNotEqual(match.group(0), "measure") + + def test_while_loop_fails(self): + qc = QuantumCircuit(1, 1) + with qc.while_loop((qc.cregs[0], 0)): + qc.x(0) + with self.assertRaisesRegex( + qasm2.QASM2ExportError, "does not support control-flow constructs" + ): + qasm2.dumps(qc) + + def test_for_loop_fails(self): + qc = QuantumCircuit(1) + with qc.for_loop(range(3)): + qc.x(0) + with self.assertRaisesRegex( + qasm2.QASM2ExportError, "does not support control-flow constructs" + ): + qasm2.dumps(qc) + + def test_switch_case_fails(self): + qc = QuantumCircuit(1, 2) + with qc.switch(qc.cregs[0]) as case: + with case(0): + qc.x(0) + with case(1): + qc.z(0) + with case(case.DEFAULT): + qc.h(0) + with self.assertRaisesRegex( + qasm2.QASM2ExportError, "does not support control-flow constructs" + ): + qasm2.dumps(qc) + + def test_else_fails(self): + qc = QuantumCircuit(1, 2) + with qc.if_test((qc.cregs[0], 0)) as else_: + qc.x(0) + with else_: + qc.z(0) + with self.assertRaisesRegex(qasm2.QASM2ExportError, "does not support 'else' statements"): + qasm2.dumps(qc) + + def test_compound_if_fails(self): + # This test could be relaxed in the future by unrolling the `if` into multiple statements. + qc = QuantumCircuit(1, 1) + with qc.if_test((qc.cregs[0], 0)): + qc.x(0) + qc.z(0) + with self.assertRaisesRegex(qasm2.QASM2ExportError, "only supports conditionals on single"): + qasm2.dumps(qc) + + def test_bit_condition_fails(self): + qc = QuantumCircuit(1, 2) + with qc.if_test((qc.clbits[0], False)): + qc.x(0) + with self.assertRaisesRegex(qasm2.QASM2ExportError, "only supports register-equality"): + qasm2.dumps(qc) + + def test_expr_condition_fails(self): + qc = QuantumCircuit(1) + with qc.if_test(expr.lift(True)): + qc.x(0) + with self.assertRaisesRegex(qasm2.QASM2ExportError, "only supports register-equality"): + qasm2.dumps(qc) + + def test_conditional_barrier_fails(self): + qc = QuantumCircuit(1, 1) + with qc.if_test((qc.cregs[0], 0)): + qc.barrier() + with self.assertRaisesRegex(qasm2.QASM2ExportError, "barriers cannot be conditional"): + qasm2.dumps(qc) + + def test_nested_if_fails(self): + qc = QuantumCircuit(1, 1) + with qc.if_test((qc.cregs[0], 0)), qc.if_test((qc.cregs[0], 1)): + qc.x(0) + with self.assertRaisesRegex(qasm2.QASM2ExportError, "does not support nested conditionals"): + qasm2.dumps(qc) + if __name__ == "__main__": unittest.main() From 1c4a96b028277f1a42800d0e641906405de2985d Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 11 Dec 2025 18:58:18 +0100 Subject: [PATCH 2/2] Specify supported CF in error message --- qiskit/qasm2/export.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit/qasm2/export.py b/qiskit/qasm2/export.py index 75f820ccf695..fe7389202bd9 100644 --- a/qiskit/qasm2/export.py +++ b/qiskit/qasm2/export.py @@ -172,7 +172,9 @@ def qasm_line(instruction) -> str | None: if instruction.is_control_flow(): operation = instruction.operation if operation.name != "if_else": - raise QASM2ExportError("OpenQASM 2 does not support control-flow constructs") + raise QASM2ExportError( + "OpenQASM 2 does not support control-flow constructs outside if_else" + ) if isinstance(operation.condition, expr.Expr) or isinstance( operation.condition[0], Clbit ):