Skip to content
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
68 changes: 50 additions & 18 deletions qiskit/qasm2/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand All @@ -165,25 +167,55 @@ 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 outside if_else"
)
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")
Comment thread
kaldag marked this conversation as resolved.
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(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's stretching my memory, but were we previously able to do c_if on InstructionSets (i.e. multiple instructions) or do we have the same functionality as before if we only allow a single instruction?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old InstructionSet.c_if (which doesn't exist any more) used to set the .condition field individually, which corresponded to the OQ2 model (and this one) of only allowing if (<reg> == <val>) <gate>; - there's no blocks in OQ2.

"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())

Expand Down
6 changes: 6 additions & 0 deletions releasenotes/notes/qasm2-if-285d10169acb3f8d.yaml
Original file line number Diff line number Diff line change
@@ -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.
132 changes: 132 additions & 0 deletions test/python/qasm2/test_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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])
Expand All @@ -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];
Expand Down Expand Up @@ -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)

Comment thread
kaldag marked this conversation as resolved.
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()