Dynamic circuits basics with OpenQASM 3

Introduction to OpenQASM 3

OpenQASM 3 is an imperative programming language designed for near-term quantum computing algorithms and applications. Quantum programs are described using the measurement-based quantum circuit model with support for classical feed-forward flow control based on measurement outcomes.

In other words, it is an assembly language to represent quantum instructions. A file with the .qasm extension can be written as a source string to run programs.

An OpenQASM 3 file consists of a header and the needed code.

OPENQASM 3.0;

// Some code

Know before you begin

Add imports and load backend information that will be used in this section:

# Do the necessary imports
from qiskit_ibm_provider import IBMProvider
from qiskit.tools.monitor import job_monitor
from qiskit.visualization import plot_histogram

# Ignoring warnings
import sys
import warnings

if not sys.warnoptions:
    warnings.simplefilter("ignore")
# Note: This can be any hub/group/project that has access to the required device and Qiskit Runtime.
hub = "<hub>"
group = "<group>"
project = "<project>"
backend_name = "<your backend>"
hgp = f"{hub}/{group}/{project}"
# load provider and backend
provider = IBMProvider()
backend = provider.get_backend(backend_name, instance=hgp)
print(f"Using backend {backend.name}")
# helper function to submit our program string to the backend's qasm3-runner runtime program
def run_qasm3_program(program_str, runtime_backend=backend):
    shots = 1000
    job = backend.run(program_str, shots=shots, dynamic=True)
    print(f\"Runtime job id: {job.job_id()}\")
    job_monitor(job)
    return job

We can dump circuits from Qiskit to OpenQASM 3. The exporter has several configuration settings; however, those below are typically valid for IBM Quantum backends.

When debugging your program, it is very useful to be able to dump a circuit to a QASM source string.

from qiskit import qasm3, QuantumCircuit, transpile

# Creating a bell circuit
qc_bell = QuantumCircuit(2, 2)
qc_bell.h(0)
qc_bell.cx(0, 1)
qc_bell.measure(0, 0)
qc_bell.measure(0, 1)

qc_bell = transpile(qc_bell, backend)
qc_bell.draw(output="mpl", idle_wires=False)
../../../../_images/output_9_0.png
exporter = qasm3.Exporter(
    disable_constants=True, basis_gates=backend.configuration().basis_gates
)

print(qasm3_bell := exporter.dumps(qc_bell))
OPENQASM 3;
include "stdgates.inc";
bit[2] c;
rz(1.5707963267949) $0;
sx $0;
rz(1.5707963267949) $0;
cx $0, $1;
c[0] = measure $0;
c[1] = measure $0;
qasm3_bell_job = run_qasm3_program(qasm3_bell)
Runtime job id: cdp6bgv77nr2iqqi4ju0
Job Status: job has successfully run
qasm3_bell_result = qasm3_bell_job.result()
print(f"Preparation results: {qasm3_bell_result.get_counts(0)}")
Preparation results: {'0': 501, '1': 8, '10': 1, '11': 490}

QASM 3 programs may be submitted as strings to the backend using Qiskit Runtime’s qasm3-runner program.

my_first_qasm3_program = """
OPENQASM 3.0;

qubit $0;
bit a0;

x $0;
a0 = measure $0;

"""
job = run_qasm3_program(my_first_qasm3_program)
Runtime job id: cdp6blpic1o4jpihr91g
Job Status: job has successfully run
result = job.result()
print(f"Preparation results: {result.get_counts(0)}")
Preparation results: {'0': 60, '1': 940}

OpenQASM 3 in-depth

Let’s understand the language statements supported by OpenQASM3 and execute them on the hardware if supported. To see what features of OpenQASM 3 are currently supported in hardware, please see the feature support table.

Variables

Let’s learn more about the variable support in OpenQASM 3. Variables are used to assign values within a program. Variables representing any classical type can be initialized on declaration.

To declare a qubit, we must do

qubit $0;
qubit $1;

Other declaration of classical variables are

int[32] a;
bit[3] c = "001";
bool my_bool = false;

for an integer variable a, a register of three bits width c, and a Boolean variable my_bool.

Control flow

OpenQASM 3 also supports looping and branching.

If-else statements

The statement if (bool) <true-body> branches the program if the Boolean evaluates to true, and may optionally be followed by else <false-body>. Both true-body and false-body can be a single statement terminated by a semicolon, or a program block of several statements { stmt1; stmt2; }.

bool target = false;
qubit $0;
h $0;
bit output = measure $0;

// example of branching
if (target == output) {
   // do something
} else {
   // do something else
}
if_example = """
OPENQASM 3.0;

bit target = 1;
qubit $0;
x $0;
bit check = measure $0;
if (check) {
   x $0;
}

bit[1] result;
result[0] = measure $0;

"""
job = run_qasm3_program(if_example)
counts = job.result().get_counts()
print(f"Measurement results: {counts}")
Runtime job id: cdp6bqnjab64ufj91ibg
Job Status: job has successfully run
Measurement results: {'0': 19, '10': 967, '100': 3, '110': 11}

For loops

The statement for <type> <name> in <values> <body> loops over the items in values, assigning each value to the variable name in subsequent iterations of the loop body.values.

The value can be a range expression in square brackets of the form [start : (step :)? stop]. Note that the range is inclusive, so stop is the last value iterated over.

int a = 1;

// here step=1
for i in [0 : 4] {
   a = i;
}

// here step=2
for i in [0 : 2 : 4] {
   a = i
}
for_loop = """
OPENQASM 3.0;

qubit $0;
for i in [0 : 4] {
   U(1.57079632679, 0.0, 3.14159265359) $0;
}

bit[1] result;
result[0] = measure $0;

"""

# U(1.57079632679, 0.0, 3.14159265359) applies a Hadamard gate
job = run_qasm3_program(for_loop)
counts = job.result().get_counts()
print(f"Measurement results: {counts}")
Runtime job id: cdp6c0losi0fkdmlo050
Job Status: job has successfully run
Measurement results: {'0': 510, '1': 490}

Classical compute

OpenQASM 3 supports classical computation on individual bits. These bits may be assigned by the program, or as a result of qubit measurement.

Individual bits are declared with the bit keyword, or registers of multiple bits may be declared using bit[size] where size indicates the number of bits in the register. For example,

bit a;
bit b;
bit c;

declares three individual bits with variable names a, b, and c, while

bit[3] result;

declares a three-bit register with the name result;.

Bits and bit registers may be assigned by the program,

bit a = 0;
a = 1;

bit[3] result = "101";
result = "010";

or they may store individual qubit measurement results.

measure_register_bits = """\
OPENQASM 3.0;
qubit $0;
qubit $1;
qubit $2;
bit a;
a = measure $0;

bit[3] result;
result[0] = measure $0;
result[1] = measure $1;
result[2] = measure $2;
"""
job = run_qasm3_program(measure_register_bits)
counts = job.result().get_counts()
print(f"Measurement results: {counts}")
Runtime job id: cdp6c54hl0189c62ej90
Job Status: job has successfully run
Measurement results: {'0': 974, '11': 10, '100': 3, '1000': 13}

The current dynamic circuits implementation does not support quantum registers; therefore, storing the result of the measurement of a quantum register

qubit[3] qubits;
bits[3] result;
result = measure qubits;

is not yet supported.

Classical computation may be applied to bits and bit registers. The current implementation supports computing bitwise operators applied to bits or bit registers. These include:

bit a;
bit b;
bit c;

Or:

c = a | b;

And:

c = a & b;

and Xor.

c = a ^ b;

These may be included as part of if statements, and grouped using parentheses:

grouped_bitwise = """\
OPENQASM 3.0;

qubit $0;

bit a = 0;
bit b = 1;
bit c;

if ((a | b)) {
   c = 0;
}

if ((a | b) ^ c) {
   c = 1;
}

bit d;
x $0;
d = measure $0;

"""
job = run_qasm3_program(grouped_bitwise)
counts = job.result().get_counts()
print(f"Measurement results: {counts}")
Runtime job id: cdp6c9pic1o4jpihrd50
Job Status: job has successfully run
Measurement results: {'0': 50, '1000': 950}

Taking the logical not of a bit is currently supported with the form:

logical_not = """\
OPENQASM 3.0;

qubit $0;

bit a;
bit b;
bit c;

a = !a;

if (a | b) {
  c = 1;
}

bit d;
x $0;
d = measure $0;

"""
job = run_qasm3_program(logical_not)
counts = job.result().get_counts()
print(f"Measurement results: {counts}")
Runtime job id: cdp6ce5n5kag9im5afvg
Job Status: job has successfully run
Measurement results: {'0': 63, '1000': 937}

At present time, taking the bit negation ~ is not supported.

Bit registers support conditions on individual bits,

bit_comparison = """\
OPENQASM 3.0;

qubit $0;

bit[3] result;
bit c;

if (result[0]) {
   c = 0;
}

bit d;
x $0;
d = measure $0;
"""
job = run_qasm3_program(bit_comparison)
counts = job.result().get_counts()
print(f"Measurement results: {counts}")
Runtime job id: cdp6cjchl0189c62emm0
Job Status: job has successfully run
Measurement results: {'0': 68, '10000': 932}

bitwise Boolean expressions,

bitwise_boolean = """\
OPENQASM 3.0;

qubit $0;

bit[3] result1;
bit[3] result2;
bit c;

if ((result1 | result2)) {
   c = 0;
}

if ((result1 & result2)) {
   c = 1;
}

bit d;
x $0;
d = measure $0;
"""
job = run_qasm3_program(bitwise_boolean)
counts = job.result().get_counts()
print(f"Measurement results: {counts}")
Runtime job id: cdp6d29ic1o4jpihri5g
Job Status: job has successfully run
Measurement results: {'0': 44, '10000000': 956}

and comparison:

bitwise_comparison = """\
OPENQASM 3.0;

qubit $0;

bit[3] result1 = "010";
bit[3] result2 = "101";
bit c;

if (result1 == result2)  {
   c = 1;
}

if (result1 > result2) {
   c = 1;
}

bit d;
x $0;
d = measure $0;
"""
job = run_qasm3_program(bitwise_comparison)
counts = job.result().get_counts()
print(f"Measurement results: {counts}")
Runtime job id: cdp6d81ic1o4jpihrj50
Job Status: job has successfully run
Measurement results: {'0': 51, '10000000': 949}

The usage of bit-string literals is currently only supported when initializing bit registers. Therefore, expressions such as

if ((result1 | result2) == "000") {
   c = 0;
}

are not yet supported.

Conditional measurement

Classical computation, Boolean expressions, and conditionals may be combined with qubit measurement to implement conditional measurement. Here the result of qubit measurements may be used to conditionally select additional qubit measurements.

qubit $0
qubit $1
bit r1;
bit r2;

r1 = measure $0;
if (r1 == "1") {
    r2 = measure $1;
}

else branches may be used to dynamically select individual qubits at runtime:

conditional_measurement_program = """\
OPENQASM 3.0;

qubit $0;
qubit $1;
qubit $2;

bit r0;
bit r1;
bit r2;

x $0;

r0 = measure $0;
if (r0) {
    r1 = measure $1;
} else {
    r2 = measure $2;
}
"""
job = run_qasm3_program(conditional_measurement_program)
counts = job.result().get_counts()
print(f"Measurement results: {counts}")
Runtime job id: cdp6dcln5kag9im5ankg
Job Status: job has successfully run
Measurement results: {'0': 75, '1': 925}

Current system limitations prevent accurate reporting of the conditional measurement results once the program terminates. The result of the measurement is stored and used during the program; however, the measurement results will not be reported to the user unless there are subsequent measurements. If there are subsequent measurements, then the conditional measurement will always be reported as 0 to the user. We expect to add support for reporting conditional measurement results in 2023.