OpenQASM Intermediate Free 6/8 in series 14 min read

Writing Quantum Circuits in OpenQASM 3

OpenQASM 3 syntax for describing quantum circuits: gate definitions, classical control flow, real-time feedback, and how QASM 3 differs from QASM 2.

What you'll learn

  • OpenQASM
  • QASM 3
  • quantum assembly
  • hardware description

Prerequisites

  • Python proficiency
  • Beginner quantum computing concepts (superposition, entanglement)
  • Linear algebra basics

What OpenQASM 3 Is

OpenQASM (Open Quantum Assembly Language) is a hardware description language for quantum circuits. It sits between high-level quantum SDKs (Qiskit, Cirq) and the physical layer of quantum hardware. When you compile a Qiskit circuit to run on IBM hardware, the backend eventually translates it to OpenQASM.

OpenQASM 3 was developed by IBM Quantum together with academic partners and released in 2021. It is the language used natively by IBM’s quantum systems and serves as a cross-platform circuit exchange format. Several other hardware providers and simulators have adopted it as an intermediate representation.

The language serves two overlapping purposes:

  1. Circuit description: A static description of a quantum algorithm as gates and measurements, equivalent to a quantum circuit diagram.
  2. Control flow language: A real-time program that can branch on measurement outcomes, loop, and apply gates conditioned on classical results mid-circuit.

QASM 2 vs QASM 3

OpenQASM 2 was sufficient for describing static circuits but had no real-time classical control. Everything that looked like a conditional was evaluated offline, not on the hardware controller.

QASM 3 adds:

FeatureQASM 2QASM 3
Classical registersBit arrays onlyFull classical types: int, float, bool, bit
Conditionalsif (creg == val) (offline)if / else evaluated in real time on the controller
LoopsNonefor and while
Mid-circuit measurementMeasurement only at endMeasure anywhere, use result immediately
Delays and timingNot supporteddelay[100ns] q;
Pulse-level accessNot supporteddefcal for calibration definitions
Subroutinesgate definitions onlydef for subroutines with classical and quantum args
Type systemNoneTyped variables, arrays

The practical consequence is that QASM 3 can express adaptive circuits: circuits that apply gates depending on measurement results obtained earlier in the same circuit execution. This is required for quantum error correction syndrome extraction and for certain quantum communication protocols.

Basic Syntax

A minimal QASM 3 program:

OPENQASM 3.0;
include "stdgates.inc";   // Standard gate library (h, cx, rx, ry, rz, etc.)

// Declare quantum and classical registers
qubit[2] q;
bit[2] c;

// Apply gates
h q[0];
cx q[0], q[1];

// Measure
c = measure q;

The include "stdgates.inc" line imports the standard library of named gates. Without it, you must define every gate from scratch using their unitary matrices.

Key syntax rules:

  • Statements end with ;
  • Comments use //
  • Qubit declarations: qubit[n] name;
  • Classical bit declarations: bit[n] name;
  • Gate application: gatename qubit_arg; or gatename qubit_arg1, qubit_arg2;
  • Measurement assigns to a classical bit: c[0] = measure q[0]; or c = measure q; (full register)

Integers and floats are also supported:

OPENQASM 3.0;
include "stdgates.inc";

qubit q;
float[64] angle = 1.5707963;   // pi/2
rx(angle) q;

Custom Gate Definitions

QASM 3 lets you define reusable gate subroutines using their action on qubits. Gate definitions must be unitary and cannot contain measurements.

OPENQASM 3.0;

// Define a custom sqrt-X gate from scratch
gate sqrtx q {
    U(pi/2, -pi/2, pi/2) q;
}

// Define a parametrized rotation composite gate
gate mygate(theta, phi) q {
    rz(phi) q;
    rx(theta) q;
}

// Define a two-qubit controlled phase gate
gate cphase(theta) ctrl, target {
    ctrl @ gphase(theta/2);
    cx ctrl, target;
    rz(-theta/2) target;
    cx ctrl, target;
    rz(theta/2) target;
}

qubit[2] q;
sqrtx q[0];
mygate(pi/4, pi/8) q[1];
cphase(pi/3) q[0], q[1];

The U(theta, phi, lambda) gate is the universal single-qubit gate primitive. Every single-qubit unitary can be expressed as U with appropriate angles.

Classical Control Flow

Classical control flow in QASM 3 runs on the hardware controller in real time, meaning the decision is made within the coherence window of the qubits, without round-tripping to a classical computer.

OPENQASM 3.0;
include "stdgates.inc";

qubit[3] q;
bit[3] c;
bit result;

// Prepare a state
h q[0];
cx q[0], q[1];

// Mid-circuit measurement of q[0]
result = measure q[0];

// Real-time conditional: apply X to q[2] only if q[0] measured as 1
if (result == 1) {
    x q[2];
}

// While loop: repeat until condition met (useful for repeat-until-success)
// This is a compile-time unrolled example for illustration
int[32] count = 0;
while (count < 3) {
    rx(pi/6) q[1];
    count += 1;
}

// Measure remaining qubits
c[1] = measure q[1];
c[2] = measure q[2];

The if block here is fundamentally different from QASM 2’s conditional gate. In QASM 2, if was syntactic sugar evaluated before circuit execution. In QASM 3, it executes on the controller chip in real time, enabling true feed-forward.

Quantum Teleportation in QASM 3

Teleportation is the canonical example of a protocol that requires real-time classical control. Alice measures two qubits and sends the two classical bits to Bob, who applies corrections in real time.

OPENQASM 3.0;
include "stdgates.inc";

// Qubits: q[0] = Alice's qubit to teleport
//         q[1] = Alice's half of the Bell pair
//         q[2] = Bob's half of the Bell pair
qubit[3] q;
bit[2] alice_bits;

// Prepare the state to teleport on q[0] (arbitrary: Ry rotation)
ry(pi/3) q[0];

// Create Bell pair between q[1] and q[2]
h q[1];
cx q[1], q[2];

// Alice's Bell measurement
cx q[0], q[1];
h q[0];

// Measure Alice's qubits
alice_bits[0] = measure q[0];
alice_bits[1] = measure q[1];

// Bob's real-time corrections based on Alice's measurement results
// These if statements execute on the controller without any classical round-trip
if (alice_bits[1] == 1) {
    x q[2];
}
if (alice_bits[0] == 1) {
    z q[2];
}

// q[2] now holds the teleported state ry(pi/3)|0>
// Verify by measuring
bit final;
final = measure q[2];

Without QASM 3’s real-time if, implementing teleportation correctly requires either postselection (discarding runs where corrections were needed) or a software round-trip to a classical computer that adds microseconds of latency, far exceeding qubit coherence times.

Using QASM 3 with Qiskit

Qiskit can both generate QASM 3 from a QuantumCircuit and parse QASM 3 back into a QuantumCircuit:

from qiskit import QuantumCircuit
from qiskit.qasm3 import dumps, loads

# Build a simple circuit in Qiskit
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])

# Export to QASM 3 string
qasm3_str = dumps(qc)
print(qasm3_str)
# Output:
# OPENQASM 3.0;
# include "stdgates.inc";
# bit[2] c;
# qubit[2] q;
# h q[0];
# cx q[0], q[1];
# c[0] = measure q[0];
# c[1] = measure q[1];

# Round-trip: parse QASM 3 back to QuantumCircuit
qc_reloaded = loads(qasm3_str)
print(qc_reloaded.draw())

# Save to file
with open("bell_state.qasm", "w") as f:
    f.write(qasm3_str)

# Load from file
with open("bell_state.qasm", "r") as f:
    qc_from_file = loads(f.read())

The round-trip is lossless for standard circuits. For circuits using QASM 3 features not yet in Qiskit’s IR (such as defcal or delay), you may need to use the QASM 3 string directly with IBM’s runtime rather than converting back to Qiskit.

Where QASM 3 Is Used

IBM Quantum systems. IBM’s hardware runs QASM 3 natively via the Qiskit Runtime. When you submit a circuit through qiskit.primitives or the Sampler/Estimator primitives, the circuit is compiled to QASM 3 before being sent to the hardware.

OpenPulse integration. QASM 3’s defcal blocks allow embedding pulse-level calibration definitions directly in the circuit file. This is used for custom gate calibrations and cross-resonance tuning.

Cross-platform exchange. Hardware providers including IonQ and Alpine Quantum Technologies have announced QASM 3 support, making it the closest thing to a universal quantum circuit interchange format currently available.

Simulators. The qasm3-tools parser and several open-source simulators accept QASM 3 directly, enabling verification of QASM 3 programs without hardware access.

Key Differences to Watch For

A few QASM 3 behaviors that differ from QASM 2 or from Qiskit defaults:

Bit ordering. QASM 3 measures into bit arrays in the same order as qubit arrays. Qiskit internally reverses bit order when displaying measurement results (least significant bit first). When comparing QASM 3 output to Qiskit counts, check the bit ordering convention.

Gate names. stdgates.inc defines a specific set of gates with specific names. cx is the controlled-X gate; ccx is Toffoli; swap is SWAP. Not all Qiskit gate names match QASM 3 names exactly.

Version header required. OPENQASM 3.0; must appear on the first line. Parsers reject files missing the version declaration.

Semicolons after blocks. Unlike C, QASM 3 does not require a semicolon after closing braces in if or gate blocks. The brace itself terminates the statement.

Summary

FeatureSyntax
Qubit declarationqubit[n] q;
Classical bitbit[n] c;
Gate applicationh q[0]; / cx q[0], q[1];
Measurementc = measure q;
Custom gategate name(params) qargs { body }
Real-time conditionalif (bit_val == 1) { ... }
Loopfor i in [0:n] { ... } / while (cond) { ... }
Delaydelay[100ns] q;
Qiskit exportqiskit.qasm3.dumps(qc)
Qiskit importqiskit.qasm3.loads(qasm_str)

OpenQASM 3 represents a significant step toward a portable, expressive quantum circuit language. Its real-time classical control features make it the right format for adaptive quantum algorithms, error correction, and any protocol that requires feed-forward between measurements and subsequent gate applications.

Was this tutorial helpful?