OpenQASM 3 Advanced Features: Gates, Timing, and Classical Control
A deep dive into OpenQASM 3's most powerful new capabilities: parameterized gate definitions, precise timing control with delay and stretch, mid-circuit measurement, classical feedforward, and a complete quantum teleportation program.
Circuit diagrams
OpenQASM 3 (Open Quantum Assembly Language 3.0) is a major redesign of the de-facto standard for describing quantum circuits. Where OpenQASM 2.0 was essentially a flat list of gate instructions, version 3 introduces a fully-featured classical control language, precise timing primitives, and a type system capable of expressing the complex programs that real quantum hardware needs to run.
OpenQASM 3 Grammar Deep Dive
Every valid OpenQASM 3 program follows a well-defined structure. The grammar requires a version declaration as the very first non-comment statement, followed by optional includes, then declarations and executable statements.
Program Structure
A canonical OpenQASM 3 program contains these sections in order:
OPENQASM 3.0; // 1. Version declaration (required, must be first)
include "stdgates.inc"; // 2. Standard library includes
// 3. Input/output declarations
input angle[20] theta;
output bit[2] result;
// 4. Gate and subroutine definitions
gate mygate(phi) q {
rz(phi) q;
h q;
}
// 5. Qubit and classical declarations
qubit[2] q;
bit[2] c;
// 6. Executable statements (gates, measurements, control flow)
mygate(theta) q[0];
result = measure q;
The version declaration OPENQASM 3.0; must appear on the first non-comment line. The parser rejects any program without it. Note the 3.0 format: writing OPENQASM 3; without the .0 is invalid.
The Standard Library: stdgates.inc
The include "stdgates.inc" directive imports the standard gate library defined by the OpenQASM specification. Without this include, only the built-in U gate and gphase instruction are available. The standard library defines these gates:
// Contents of stdgates.inc (summarized)
// Single-qubit Pauli gates
gate x a { U(pi, 0, pi) a; }
gate y a { U(pi, pi/2, pi/2) a; }
gate z a { gphase(-pi/2); U(0, 0, pi) a; }
// Hadamard
gate h a { U(pi/2, 0, pi) a; }
// Phase gates
gate s a { pow(1/2) @ z a; }
gate t a { pow(1/4) @ z a; }
gate sdg a { inv @ s a; }
gate tdg a { inv @ t a; }
// Rotation gates
gate rx(theta) a { U(theta, -pi/2, pi/2) a; }
gate ry(theta) a { U(theta, 0, 0) a; }
gate rz(lambda) a { gphase(-lambda/2); U(0, 0, lambda) a; }
// Two-qubit gates
gate cx a, b { ctrl @ x a, b; }
gate cy a, b { ctrl @ y a, b; }
gate cz a, b { ctrl @ z a, b; }
gate swap a, b { cx a, b; cx b, a; cx a, b; }
gate ccx a, b, c { ctrl @ ctrl @ x a, b, c; }
// Parameterized two-qubit gates
gate cp(lambda) a, b { ctrl @ rz(lambda) a, b; }
gate crx(theta) a, b { ctrl @ rx(theta) a, b; }
gate cry(theta) a, b { ctrl @ ry(theta) a, b; }
gate crz(theta) a, b { ctrl @ rz(theta) a, b; }
The U gate is the built-in universal single-qubit gate with the parameterization . All other gates in stdgates.inc are defined in terms of U and gphase. The gphase instruction applies a global phase, which matters when gates are used inside controlled contexts.
Calibration Blocks
Programs that target specific hardware can include calibration blocks that define pulse-level implementations:
OPENQASM 3.0;
include "stdgates.inc";
// cal blocks contain pulse-level declarations
cal {
extern port d0;
extern frame drive_frame;
}
// defcal blocks define pulse-level gate implementations
defcal h $0 {
play(drive_frame, gaussian(160dt, 0.15, 40dt));
}
The cal block introduces pulse-level names and waveforms. The defcal block binds a gate to a specific pulse sequence on physical qubits (denoted with $ prefix).
What Changed from OpenQASM 2.0
OpenQASM 2.0’s grammar was small by design: qubits, classical bits, gate calls, measurement, and a single if statement conditioned on an integer comparison. This was sufficient for describing static circuits but inadequate for anything requiring real-time classical processing.
OpenQASM 3 adds:
- A proper type system:
qubit,qubit[n],bit,bit[n],int,uint,float,angle,bool,duration,stretch - Gate definitions with angle parameters and gate-level modifiers (
inv @,pow(n) @,ctrl @) - Timing control:
delay,barrier,stretchtypes for scheduling - Mid-circuit measurement with measurement results usable in classical control flow
whileloops andforloops over classical registersexternfunctions for calling classical subroutines on the control processorinputandoutputdeclarations for parameterized circuits passed to hardwareboxblocks for grouping operations with timing constraints
Type System and Variable Declarations
The new type system is one of the most practically important changes. In OpenQASM 2.0, qubits and classical bits were declared as registers with fixed sizes. In version 3, you can declare individual qubits:
OPENQASM 3.0;
// Individual qubits
qubit q;
qubit[3] reg;
// Classical types
bit c;
bit[3] creg;
int[32] count = 0;
float[64] theta = 0.5;
angle[20] phi; // fixed-point angle with 20-bit precision
bool flag = false;
The angle type is new and represents a value in with a fixed number of bits. It enables efficient angle arithmetic on classical control hardware without floating-point overhead.
Angle Arithmetic and Wrapping
The angle[n] type stores values in the range using bits of precision. Arithmetic on angles wraps automatically modulo , which mirrors how rotation gates behave physically. This wrapping happens at the hardware level without any explicit modular reduction.
OPENQASM 3.0;
include "stdgates.inc";
qubit q;
angle[20] a = pi; // pi radians
angle[20] b = 3 * pi / 2; // 3*pi/2 radians
// Adding angles wraps modulo 2*pi
angle[20] c = a + b; // pi + 3*pi/2 = 5*pi/2 -> wraps to pi/2
// This means rz(c) applies a rotation of pi/2, not 5*pi/2
rz(c) q;
// Doubling also wraps: 2 * (3*pi/2) = 3*pi -> wraps to pi
angle[20] d = 2 * b; // equals pi after wrapping
Compare this to using float[64] for the same purpose:
// Using float: no automatic wrapping, higher precision, more hardware cost
float[64] theta_f = 5.0 * pi / 2.0;
// theta_f stores 7.853981... -- the full value, no wrapping
// The gate itself reduces mod 2*pi, but intermediate calculations do not
// Using angle: wrapping is built into the type
angle[20] theta_a = 5 * pi / 2;
// theta_a stores the equivalent of pi/2 directly
// For VQE or QAOA where you run millions of circuits,
// angle[20] uses fixed-point arithmetic on the FPGA controller
// while float[64] requires a floating-point unit
When does angle beat float? Use angle when the parameter feeds directly into rotation gates and the classical controller needs to perform fast arithmetic in the feedback loop. Use float when you need full dynamic range or when intermediate calculations involve non-angular quantities.
Duration and Stretch Types
The duration type stores time values with explicit units. The stretch type represents a flexible duration that the compiler resolves to satisfy alignment constraints.
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
// Duration literals support multiple units
duration t1 = 100ns;
duration t2 = 200us;
duration t3 = 50dt; // dt is the hardware-specific timestep
// Durations support arithmetic
duration total = t1 + t1; // 200ns
duration half = t1 / 2; // 50ns
// Stretch is a variable-length duration
// The compiler adjusts it to align operations across qubits
stretch s;
delay[s] q[0]; // Compiler fills this to match timing of q[1]
cx q[0], q[1];
Stretch is particularly useful for ensuring that two-qubit gates begin at the same time across both qubits, even when the single-qubit operations preceding them take different amounts of time.
Bit Slicing
The bit[n] type supports slicing and indexing, which makes it possible to work with subsets of measurement results:
OPENQASM 3.0;
include "stdgates.inc";
qubit[8] q;
bit[8] c;
// Measure all qubits
c = measure q;
// Access individual bits
bit b0 = c[0];
bit b7 = c[7];
// Slicing extracts a contiguous range
bit[4] low_nibble = c[0:3]; // bits 0, 1, 2, 3
bit[4] high_nibble = c[4:7]; // bits 4, 5, 6, 7
// Use slices in conditionals
if (c[0:2] == 3'b101) {
x q[3];
}
Integer Arithmetic in Classical Control Flow
Classical integer types support the full range of arithmetic and comparison operators, enabling complex control logic:
OPENQASM 3.0;
include "stdgates.inc";
qubit[4] q;
bit[4] c;
int[32] syndrome = 0;
int[32] correction_type;
// Measure syndrome qubits
c[0:1] = measure q[0:1];
// Compute syndrome value using bit arithmetic
syndrome = c[0] + 2 * c[1];
// Branch on syndrome to determine correction
if (syndrome == 0) {
correction_type = 0; // No error
} else if (syndrome == 1) {
correction_type = 1;
x q[2]; // Correct bit flip on data qubit 0
} else if (syndrome == 2) {
correction_type = 2;
x q[3]; // Correct bit flip on data qubit 1
} else {
correction_type = 3;
x q[2];
x q[3];
}
Parameterized Gate Definitions
Gate definitions in OpenQASM 3 can take both angle parameters and qubit arguments, just as in version 2, but now support modifiers for inversion, power, and controlled variants:
// Define a parameterized rotation
gate rz(theta) q {
gphase(-theta / 2);
U(0, 0, theta) q;
}
// Define a controlled-phase gate
gate cphase(lambda) ctrl, target {
ctrl @ rz(lambda) target;
}
// Apply the inverse of rz
inv @ rz(pi / 4) q[0];
// Apply rz squared (power modifier)
pow(2) @ rz(pi / 8) q[0];
The ctrl @ modifier wraps any gate in a controlled version, eliminating the need to define separate controlled variants by hand.
Gate Modifiers: inv, pow, and ctrl
OpenQASM 3 provides three gate modifiers that compose with any gate definition. These modifiers generate new gates from existing ones without requiring separate definitions.
The inv Modifier
The inv @ modifier applies the inverse (adjoint) of a gate. For unitary , inv @ U applies .
OPENQASM 3.0;
include "stdgates.inc";
qubit q;
// S-dagger is the inverse of S
// S = diag(1, i), so S-dagger = diag(1, -i)
inv @ s q; // equivalent to sdg q
// T-dagger is the inverse of T
inv @ t q; // equivalent to tdg q
// inv on a composite gate reverses gate order and inverts each gate
gate my_circuit(theta) q {
h q;
rz(theta) q;
h q;
}
// inv @ my_circuit(theta) applies: h, rz(-theta), h
// (reverse order, invert each gate)
inv @ my_circuit(pi / 4) q;
The pow Modifier
The pow(k) @ modifier applies a gate times. For integer , this is straightforward repeated application. For fractional , the compiler decomposes the gate into its eigenvalues and raises them to the -th power.
OPENQASM 3.0;
include "stdgates.inc";
qubit q;
// T^2 = S (since T = diag(1, e^{i*pi/4}) and S = diag(1, e^{i*pi/2}))
pow(2) @ t q; // equivalent to s q
// T^4 = Z (since Z = diag(1, e^{i*pi}))
pow(4) @ t q; // equivalent to z q
// sqrt(X) using pow(1/2)
pow(1/2) @ x q; // applies sqrt(X) = (1/2) * [[1+i, 1-i], [1-i, 1+i]]
// pow(0) is the identity (useful in parameterized contexts)
pow(0) @ x q; // does nothing
// pow(-1) is the inverse
pow(-1) @ s q; // equivalent to inv @ s q, equivalent to sdg q
The ctrl Modifier
The ctrl @ modifier adds a control qubit to any gate. Multiple ctrl @ modifiers can be stacked to create multi-controlled gates.
OPENQASM 3.0;
include "stdgates.inc";
qubit[4] q;
// Controlled-X (CNOT)
ctrl @ x q[0], q[1]; // equivalent to cx q[0], q[1]
// Controlled-controlled-X (Toffoli / CCX)
ctrl @ ctrl @ x q[0], q[1], q[2]; // equivalent to ccx q[0], q[1], q[2]
// Triply-controlled X (C3X)
ctrl @ ctrl @ ctrl @ x q[0], q[1], q[2], q[3];
// Controlled-S gate
ctrl @ s q[0], q[1];
// Controlled rotation
ctrl @ rz(pi / 4) q[0], q[1];
// Combining modifiers: controlled inverse
ctrl @ inv @ s q[0], q[1]; // controlled S-dagger
Global Phase Semantics
When you define a gate using gphase, the global phase becomes physically meaningful inside a ctrl @ context. A global phase on a gate becomes a relative phase on the control qubit when the gate is controlled:
OPENQASM 3.0;
include "stdgates.inc";
// rz includes a gphase term in its definition:
// gate rz(theta) q { gphase(-theta/2); U(0, 0, theta) q; }
// When controlled, this global phase becomes a phase on the control qubit
qubit[2] q;
ctrl @ rz(pi / 2) q[0], q[1];
// This is NOT the same as just controlling the U(0, 0, pi/2) part.
// The gphase(-pi/4) contributes a phase shift on |1> of the control.
This is why stdgates.inc carefully includes gphase terms in gate definitions: they ensure that controlled versions produce the correct unitary.
Timing: delay, stretch, and barrier
Quantum hardware has strict timing requirements. OpenQASM 3 introduces duration and stretch types to describe timing constraints in a hardware-agnostic way.
// duration literals
duration d1 = 100ns;
duration d2 = 50dt; // dt = device timestep
// Insert a delay of 200 nanoseconds on qubit q[0]
delay[200ns] q[0];
// Stretch: a flexible delay that the compiler can adjust
// to meet alignment constraints
stretch s;
delay[s] q[0];
// barrier prevents reordering across it, like OpenQASM 2.0
barrier q[0], q[1];
// box groups operations and declares a timing window
box[500ns] {
rz(pi / 2) q[0];
cx q[0], q[1];
}
Delays are critical for T1/T2 measurement sequences where you need precise idle times, and for dynamical decoupling where pulse spacing must be controlled at the nanosecond level.
Advanced Timing: Dynamical Decoupling
Dynamical decoupling (DD) suppresses decoherence by inserting carefully timed refocusing pulses during idle periods. The key idea is that a sequence of pulses averages out the effect of low-frequency noise on the qubit’s phase.
The CPMG (Carr-Purcell-Meiboom-Gill) sequence is a standard DD protocol. It inserts equally spaced (or ) pulses during an idle window of total duration . Each pulse refocuses dephasing accumulated during the preceding interval. The spacing between pulses must be much shorter than the qubit’s time for effective decoupling.
OPENQASM 3.0;
include "stdgates.inc";
// CPMG dynamical decoupling sequence
// Total idle time T is divided into 2*N intervals of length tau = T/(2*N)
// Pattern: tau - X - 2*tau - X - 2*tau - ... - 2*tau - X - tau
qubit q; // The qubit to protect during idle time
qubit[2] spectator; // Other qubits doing useful work
duration T = 2000ns; // Total idle window on q
int[32] N = 4; // Number of refocusing pulses
duration tau = 250ns; // T / (2*N) = 2000ns / 8 = 250ns
// Initial half-interval
delay[tau] q;
// First N-1 pulses with 2*tau spacing
x q;
delay[500ns] q; // 2 * tau between pulses
x q;
delay[500ns] q;
x q;
delay[500ns] q;
// Final pulse and half-interval
x q;
delay[tau] q;
// The net effect: any phase accumulated between pulses is refocused.
// For quasi-static dephasing noise (spectral weight at low frequencies),
// the CPMG sequence reduces the effective dephasing rate by a factor
// proportional to N.
Why must the delays be precisely timed relative to ? If the inter-pulse spacing approaches , the qubit loses coherence between pulses and the refocusing becomes ineffective. Conversely, if the pulses are spaced too closely, gate errors from imperfect pulses accumulate and degrade the state. The optimal spacing balances these two effects, typically keeping .
A more hardware-aware version uses barrier to prevent the compiler from reordering the DD pulses:
OPENQASM 3.0;
include "stdgates.inc";
qubit q;
// XY-4 sequence: X - Y - X - Y (suppresses both dephasing and amplitude noise)
duration tau = 100ns;
delay[tau] q;
barrier q; // Prevent compiler from moving the X gate
x q;
barrier q;
delay[2 * tau] q;
barrier q;
y q;
barrier q;
delay[2 * tau] q;
barrier q;
x q;
barrier q;
delay[2 * tau] q;
barrier q;
y q;
barrier q;
delay[tau] q;
The barrier directives prevent the transpiler from merging, canceling, or reordering the DD pulses, which would defeat their purpose.
Mid-Circuit Measurement and Classical Control
This is the feature that makes OpenQASM 3 qualitatively different from its predecessor. Measurement results can be stored in classical bits and used immediately to condition future gate operations:
qubit[2] q;
bit[2] c;
// Mid-circuit measurement: result goes into c[0]
c[0] = measure q[0];
// Classically conditioned gate, applied in real time
if (c[0] == 1) {
x q[1];
}
// while loop conditioned on a classical register
int[32] i = 0;
while (i < 5) {
h q[0];
c[0] = measure q[0];
i += 1;
}
The if/else and while constructs execute on the classical controller embedded in the quantum hardware, so their latency is much shorter than sending results to a host computer and back.
For Loops and Arrays
OpenQASM 3 supports for loops that iterate over ranges and discrete sets. This enables compact description of repetitive circuit patterns.
Iterating Over Ranges
OPENQASM 3.0;
include "stdgates.inc";
qubit[8] q;
// Apply Hadamard to all qubits using a range
for int i in [0:7] {
h q[i];
}
// Apply RZ with increasing angles
for int i in [0:7] {
rz(pi * i / 8) q[i];
}
// Step through every other qubit
for int i in [0:2:7] {
x q[i]; // applies X to q[0], q[2], q[4], q[6]
}
Variational Circuit with Array of Angles
A variational ansatz circuit applies parameterized rotations in layers. Using for loops with an array of angles keeps the description compact:
OPENQASM 3.0;
include "stdgates.inc";
// Four-qubit hardware-efficient ansatz
input angle[20] params[12]; // 4 qubits * 3 layers of rotation
output bit[4] result;
qubit[4] q;
int[32] idx = 0;
// Three layers of rotation + entanglement
for int layer in [0:2] {
// Single-qubit rotation layer
for int i in [0:3] {
ry(params[idx]) q[i];
idx += 1;
}
// Entangling layer: linear chain of CNOTs
for int i in [0:2] {
cx q[i], q[i + 1];
}
}
result = measure q;
The equivalent static circuit would require writing out all 12 rotation gates and 9 CNOT gates individually. The loop version is shorter, less error-prone, and makes the circuit structure immediately visible.
Classical Subroutines with def
OpenQASM 3 supports classical function definitions using the def keyword. These functions run on the classical controller and can compute values used in subsequent gate parameters.
OPENQASM 3.0;
include "stdgates.inc";
// Define a classical function that maps syndrome bits to a correction angle
def correction_angle(bit[2] syndrome) -> angle[20] {
int[32] s = syndrome[0] + 2 * syndrome[1];
angle[20] result;
if (s == 0) {
result = 0;
} else if (s == 1) {
result = pi / 2;
} else if (s == 2) {
result = pi;
} else {
result = 3 * pi / 2;
}
return result;
}
qubit[4] q;
bit[2] syndrome_bits;
// Prepare state and introduce error
h q[0];
cx q[0], q[1];
// Measure syndrome
syndrome_bits[0] = measure q[2];
syndrome_bits[1] = measure q[3];
// Use classical function to determine correction
angle[20] phi = correction_angle(syndrome_bits);
rz(phi) q[0];
Adaptive Measurement with def
A common pattern combines mid-circuit measurement with a classical subroutine that decides the next operation. This is the foundation of adaptive quantum protocols:
OPENQASM 3.0;
include "stdgates.inc";
// Determines the Pauli frame correction based on two measurement bits
def pauli_correction(bit m1, bit m2) -> int[32] {
// Returns: 0 = I, 1 = X, 2 = Z, 3 = XZ
return m1 + 2 * m2;
}
qubit[3] q;
bit b1;
bit b2;
// Prepare entangled state
h q[0];
cx q[0], q[1];
cx q[0], q[2];
// Mid-circuit measurements
b1 = measure q[0];
b2 = measure q[1];
// Classical logic determines correction
int[32] correction = pauli_correction(b1, b2);
if (correction == 1) {
x q[2];
} else if (correction == 2) {
z q[2];
} else if (correction == 3) {
x q[2];
z q[2];
}
The def keyword defines functions that compile to the classical controller’s instruction set. These differ from extern functions in that extern calls out to external code (such as an FPGA coprocessor), while def functions are written entirely in OpenQASM 3 and compiled along with the rest of the program.
extern Functions
The extern keyword declares a classical function that executes on the control processor, allowing computation that is not expressible in OpenQASM itself:
extern compute_angle(int[32]) -> angle[20];
int[32] data = 42;
angle[20] computed_phi = compute_angle(data);
rz(computed_phi) q[0];
This enables workflows where classical processing (e.g., decoding syndrome measurements in error correction) happens on dedicated FPGA logic and feeds directly into subsequent gate selection.
input and output: Parameterized Circuits
The input declaration lets you specify parameters that are bound when the circuit is submitted to hardware, avoiding the need to recompile for each parameter value:
input angle[20] theta;
input angle[20] phi;
output bit[2] result;
qubit[2] q;
rz(theta) q[0];
ry(phi) q[1];
cx q[0], q[1];
result = measure q;
This is essential for variational algorithms like VQE where the same circuit structure is evaluated at thousands of parameter points.
VQE Ansatz with Input Parameters
The Variational Quantum Eigensolver (VQE) evaluates a parameterized circuit at many parameter points, using a classical optimizer to minimize the expectation value of a Hamiltonian. The input keyword lets you submit the circuit once and rebind parameters for each evaluation.
Hardware-Efficient Ansatz
A hardware-efficient ansatz uses the native gate set of the target device. It alternates layers of parameterized single-qubit rotations with layers of entangling gates:
OPENQASM 3.0;
include "stdgates.inc";
// Hardware-efficient ansatz for 4-qubit VQE
// Two rotation layers, each with RY and RZ per qubit = 2 * 4 * 2 = 16 params
input angle[20] ry_params[8]; // RY angles: 2 layers * 4 qubits
input angle[20] rz_params[8]; // RZ angles: 2 layers * 4 qubits
output bit[4] result;
qubit[4] q;
// Layer 1: single-qubit rotations
for int i in [0:3] {
ry(ry_params[i]) q[i];
rz(rz_params[i]) q[i];
}
// Layer 1: entanglement (circular CNOT chain)
cx q[0], q[1];
cx q[1], q[2];
cx q[2], q[3];
cx q[3], q[0];
// Layer 2: single-qubit rotations
for int i in [0:3] {
ry(ry_params[i + 4]) q[i];
rz(rz_params[i + 4]) q[i];
}
// Layer 2: entanglement
cx q[0], q[1];
cx q[1], q[2];
cx q[2], q[3];
cx q[3], q[0];
// Measure in computational basis
result = measure q;
The classical optimizer (running on the host, not in OpenQASM) evaluates the expectation value from measurement statistics, computes new parameter values, and rebinds them for the next circuit evaluation. The circuit itself never needs recompilation.
UCCSD-Style Ansatz
A chemically-inspired ansatz based on the Unitary Coupled Cluster Singles and Doubles (UCCSD) method encodes excitation operators as parameterized gate sequences. For a minimal two-qubit model (single excitation between orbitals 0 and 1):
OPENQASM 3.0;
include "stdgates.inc";
// UCCSD-style single excitation for 4-qubit H2 in minimal basis
// The single excitation operator exp(i*theta*(a1^dag a0 - a0^dag a1))
// maps to a sequence of CNOT and RY gates after Jordan-Wigner transform
input angle[20] t1; // Single excitation amplitude
input angle[20] t2; // Double excitation amplitude
output bit[4] result;
qubit[4] q;
// Prepare Hartree-Fock reference state |0011>
x q[0];
x q[1];
// Single excitation: orbitals 0 -> 2
// This implements exp(i * t1 * (a2^dag a0 - h.c.)) via JW transform
cx q[0], q[2];
ry(t1 / 2) q[2];
cx q[0], q[2];
ry(-t1 / 2) q[2];
// Double excitation: orbitals (0,1) -> (2,3)
// Simplified decomposition of the double excitation operator
cx q[3], q[2];
cx q[2], q[1];
cx q[1], q[0];
ry(t2 / 8) q[0];
cx q[1], q[0];
ry(-t2 / 8) q[0];
cx q[2], q[1];
cx q[1], q[0];
ry(t2 / 8) q[0];
cx q[1], q[0];
ry(-t2 / 8) q[0];
cx q[3], q[2];
result = measure q;
The key advantage of input over hardcoded angles: the host program calls the backend once to compile the circuit, then evaluates it at hundreds or thousands of parameter points by rebinding t1 and t2. Compilation is expensive (transpilation, routing, optimization), so avoiding recompilation at each step of the optimizer saves significant wall-clock time.
Full Example: Quantum Teleportation with Classical Feedforward
Here is a complete quantum teleportation circuit in OpenQASM 3. This program is impossible to express correctly in OpenQASM 2.0 because it requires mid-circuit measurement results to control subsequent gates in real time.
OPENQASM 3.0;
// Declare three qubits: message (msg), Alice's half (a), Bob's half (b)
qubit msg;
qubit a;
qubit b;
bit c_a;
bit c_b;
bit verified;
// Step 1: Prepare a Bell pair between Alice and Bob
h a;
cx a, b;
// Step 2: Prepare the message qubit in an arbitrary state
// (In practice, this is the state to be teleported)
h msg;
rz(pi / 3) msg;
// Step 3: Bell measurement on msg and a
cx msg, a;
h msg;
// Step 4: Mid-circuit measurements
c_a = measure a;
c_b = measure msg;
// Step 5: Classical feedforward corrections to Bob's qubit
// These execute on the classical controller without round-tripping to host
if (c_a == 1) {
x b;
}
if (c_b == 1) {
z b;
}
// Bob's qubit now holds the teleported state
// Verify by applying inverse preparation and measuring
rz(-pi / 3) b;
h b;
verified = measure b;
In OpenQASM 2.0, the if statements on c_a and c_b could only condition on the integer value of a full classical register, and they were often interpreted as post-processing rather than real-time control. OpenQASM 3 makes the real-time semantics explicit and well-defined.
Quantum Error Correction: 3-Qubit Bit-Flip Code
Quantum error correction (QEC) requires mid-circuit measurement, classical logic, and conditional gates, all features that OpenQASM 3 handles natively. The 3-qubit bit-flip code protects a single logical qubit against single bit-flip () errors.
Encoding
The logical state is encoded into three physical qubits as using two CNOT gates:
OPENQASM 3.0;
include "stdgates.inc";
// 3-qubit bit-flip code with syndrome measurement and correction
qubit[3] data; // Three data qubits encoding one logical qubit
qubit[2] ancilla; // Two ancilla qubits for syndrome extraction
bit[2] syndrome; // Syndrome measurement results
bit result; // Final logical measurement
// Step 1: Prepare the logical state to protect
// Encode |psi> = alpha|0> + beta|1> as alpha|000> + beta|111>
// Start by preparing data[0] in the desired state
h data[0]; // Example: prepare |+> state
rz(pi / 3) data[0]; // Arbitrary state to protect
// Step 2: Encode using CNOT gates
// data[0] is the original qubit; data[1] and data[2] are redundant copies
cx data[0], data[1]; // |psi,0,0> -> alpha|000> + beta|110>
cx data[0], data[2]; // -> alpha|000> + beta|111>
Syndrome Extraction
The syndrome identifies which qubit (if any) experienced a bit-flip error without collapsing the encoded state. Two ancilla qubits measure the parity of qubit pairs:
- Ancilla 0 measures the parity of data[0] and data[1]
- Ancilla 1 measures the parity of data[1] and data[2]
If no error occurred, both parities are even (syndrome = 00). A single bit-flip produces a unique syndrome pattern.
// Step 3: Simulate an error (for demonstration)
// A bit-flip error on data[1]:
x data[1];
// Step 4: Syndrome extraction
// Ancilla 0 checks parity of data[0] XOR data[1]
cx data[0], ancilla[0];
cx data[1], ancilla[0];
// Ancilla 1 checks parity of data[1] XOR data[2]
cx data[1], ancilla[1];
cx data[2], ancilla[1];
// Step 5: Measure the syndrome (mid-circuit measurement)
syndrome[0] = measure ancilla[0];
syndrome[1] = measure ancilla[1];
Correction
The syndrome bits map to corrections as follows:
| syndrome[1] | syndrome[0] | Meaning | Correction |
|---|---|---|---|
| 0 | 0 | No error | None |
| 0 | 1 | data[0] flipped | X on data[0] |
| 1 | 0 | data[2] flipped | X on data[2] |
| 1 | 1 | data[1] flipped | X on data[1] |
// Step 6: Apply correction based on syndrome
if (syndrome == 2'b01) {
x data[0]; // Correct error on data[0]
} else if (syndrome == 2'b10) {
x data[2]; // Correct error on data[2]
} else if (syndrome == 2'b11) {
x data[1]; // Correct error on data[1]
}
// syndrome == 2'b00 means no error, no correction needed
// Step 7: Decode by reversing the encoding
cx data[0], data[2];
cx data[0], data[1];
// Step 8: Measure the logical qubit
result = measure data[0];
The complete program runs the full encode-error-syndrome-correct-decode cycle. On real hardware, the syndrome extraction and correction would run in a loop to handle errors that occur during the computation.
Repeat-Until-Success (RUS) Circuits
Some quantum operations cannot be implemented deterministically with a fixed gate set. Repeat-until-success circuits use a probabilistic approach: run a circuit, measure an ancilla, and retry if the measurement indicates failure.
Why RUS Matters
Certain non-Clifford rotations (such as on fault-tolerant architectures) are expensive to synthesize exactly from a discrete gate set. An RUS approach can prepare the correct state with fewer resources on average, at the cost of requiring mid-circuit measurement and classical feedback.
RUS Pattern
OPENQASM 3.0;
include "stdgates.inc";
// Repeat-until-success: prepare a specific rotation on the target qubit
// by repeatedly attempting a probabilistic circuit.
// Success is indicated by measuring the ancilla in |0>.
qubit target;
qubit ancilla;
bit flag;
bit outcome;
// Initialize
h target; // Put target in initial state
// RUS loop
bool success = false;
int[32] attempts = 0;
int[32] max_attempts = 100;
while (!success && attempts < max_attempts) {
// Reset ancilla for each attempt
reset ancilla;
// Probabilistic circuit:
// Apply entangling operation between target and ancilla
h ancilla;
cx ancilla, target;
t target;
cx ancilla, target;
h ancilla;
// Measure ancilla
flag = measure ancilla;
if (flag == 0) {
// Success: the target qubit now has the desired rotation applied
success = true;
} else {
// Failure: undo the unwanted operation on the target
// The specific correction depends on the circuit design
z target;
}
attempts += 1;
}
// At this point, target has the desired state (with high probability)
outcome = measure target;
The while loop continues until the ancilla measurement signals success. The reset instruction reinitializes the ancilla to at the start of each attempt without requiring a full qubit deallocation. The maximum attempt counter prevents infinite loops on hardware where noise might prevent convergence.
RUS circuits are particularly important for:
- Implementing non-Clifford gates in fault-tolerant architectures
- State distillation protocols that produce high-fidelity magic states
- Non-deterministic state preparation (e.g., preparing W states)
Pulse-Level Access
OpenQASM 3 includes a defcal mechanism for defining gate calibrations at the pulse level, bridging the gap between circuit-level programs and hardware-specific waveforms:
// Import the pulse framework for this device
cal {
waveform drag_pulse = drag(duration: 160dt, amplitude: 0.2,
sigma: 40dt, beta: -0.5);
}
// Define what an X gate means in terms of pulses on qubit 0
defcal x $0 {
play(drag_pulse, drive($0));
}
This capability was previously only available through device-specific APIs (like Qiskit Pulse). Having it in OpenQASM 3 means calibrations can be expressed in a portable, standardized format.
DRAG Pulse Calibration
The DRAG (Derivative Removal by Adiabatic Gate) correction is a pulse-shaping technique that reduces leakage from the computational subspace (, ) into the state of a transmon qubit. Transmons are weakly anharmonic oscillators, so a pulse that drives the transition can also excite the transition. DRAG adds a 90-degree out-of-phase quadrature component proportional to the derivative of the in-phase envelope, which destructively interferes with the leakage pathway.
A Gaussian pulse without DRAG correction:
cal {
// Simple Gaussian pulse (no DRAG)
waveform gauss_pulse = gaussian(duration: 160dt, amplitude: 0.25, sigma: 40dt);
}
defcal x $0 {
play(gauss_pulse, drive($0));
}
Adding DRAG correction:
cal {
// DRAG pulse: adds derivative component on the quadrature channel
// beta controls the strength of the DRAG correction
// Typical beta values: -0.5 to 0.5, calibrated per qubit
waveform x_drag = drag(
duration: 160dt, // Total pulse duration
amplitude: 0.25, // In-phase amplitude (calibrated for pi rotation)
sigma: 40dt, // Gaussian width
beta: -0.4 // DRAG coefficient (suppresses |2> leakage)
);
// For Y gate, same shape but with 90-degree phase offset
waveform y_drag = drag(
duration: 160dt,
amplitude: 0.25,
sigma: 40dt,
beta: -0.4
);
}
defcal x $0 {
play(x_drag, drive($0));
}
defcal y $0 {
shift_phase(drive($0), pi / 2);
play(y_drag, drive($0));
shift_phase(drive($0), -pi / 2);
}
// sqrt(X) uses half the amplitude
cal {
waveform sx_drag = drag(
duration: 160dt,
amplitude: 0.125, // Half amplitude for pi/2 rotation
sigma: 40dt,
beta: -0.4
);
}
defcal sx $0 {
play(sx_drag, drive($0));
}
CNOT Calibration with Cross-Resonance
A CNOT gate on fixed-frequency transmon hardware typically uses a cross-resonance (CR) pulse: the control qubit’s drive line is pulsed at the target qubit’s frequency. This creates a interaction that, combined with local rotations, implements CNOT.
cal {
// Cross-resonance pulse: drive control at target's frequency
waveform cr_pulse = gaussian_square(
duration: 640dt, // CR pulses are much longer than single-qubit
amplitude: 0.45,
sigma: 64dt,
width: 512dt // Flat-top portion
);
// Echo pulse to cancel unwanted ZZ and IX terms
waveform cr_echo = gaussian_square(
duration: 640dt,
amplitude: -0.45, // Negative amplitude for echo
sigma: 64dt,
width: 512dt
);
// Local rotations to convert ZX into CNOT
waveform x90_drag = drag(duration: 160dt, amplitude: 0.125, sigma: 40dt, beta: -0.4);
}
defcal cx $0, $1 {
// Echoed cross-resonance CNOT decomposition:
// CR pulse, then X on control, then negative CR pulse, then X on control
// This "echoed" sequence cancels unwanted terms in the Hamiltonian
play(cr_pulse, cross_resonance($0, $1)); // CR(+)
play(x90_drag, drive($0)); // X on control
play(x90_drag, drive($0)); // second X90 to complete X
play(cr_echo, cross_resonance($0, $1)); // CR(-)
play(x90_drag, drive($0)); // Correct control
play(x90_drag, drive($0));
// Local rotations on target to map ZX -> CNOT
shift_phase(drive($1), -pi / 2);
play(x90_drag, drive($1));
shift_phase(drive($1), pi / 2);
}
The cross_resonance($0, $1) frame directs a pulse from qubit 1’s frequency. The echoed CR protocol applies a positive CR pulse, flips the control qubit, then applies a negative CR pulse. This cancels unwanted and interaction terms while preserving the desired coupling.
OpenQASM 3 vs OpenQASM 2.0 Migration
If you have existing OpenQASM 2.0 programs, here are the key translation patterns for migrating to OpenQASM 3.
1. Qubit and Bit Declarations
OpenQASM 2.0:
OPENQASM 2.0;
include "qelib1.inc";
qreg q[3];
creg c[3];
OpenQASM 3.0:
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
bit[3] c;
The qreg/creg keywords are replaced by qubit[n]/bit[n]. The standard library changes from qelib1.inc to stdgates.inc. You can also declare individual qubits with qubit q; (no array notation).
2. Conditional Gates
OpenQASM 2.0:
// Condition on entire classical register equaling a value
measure q[0] -> c[0];
if(c==1) x q[1];
OpenQASM 3.0:
// Assignment syntax for measurement, full if/else blocks
c[0] = measure q[0];
if (c[0] == 1) {
x q[1];
} else {
z q[1];
}
OpenQASM 2.0 could only condition on an entire classical register equaling an integer. OpenQASM 3 supports conditions on individual bits, comparisons, and boolean expressions. The measurement syntax changes from measure q -> c to c = measure q.
3. Parameterized Circuits
OpenQASM 2.0:
// Parameters hardcoded into the QASM string
// Must recompile the entire program for each new angle
gate myrot(theta) q {
rz(theta) q;
}
myrot(0.5) q[0];
OpenQASM 3.0:
// Parameters declared as inputs, bound at runtime
input angle[20] theta;
gate myrot(theta_param) q {
rz(theta_param) q;
}
myrot(theta) q[0];
The input keyword allows the classical optimizer to rebind theta without recompiling the circuit. This is critical for variational algorithms.
4. Barrier Usage
OpenQASM 2.0:
// Barrier on all qubits (implicit)
barrier q;
OpenQASM 3.0:
// Barrier on specific qubits or all qubits
barrier q; // All qubits in register q
barrier q[0], q[1]; // Only specific qubits
The semantics are the same (prevent gate reordering across the barrier), but OpenQASM 3 makes it easier to apply barriers to specific subsets of qubits.
5. Measurement
OpenQASM 2.0:
// Arrow syntax, measure single qubit
measure q[0] -> c[0];
// Measure all
measure q -> c;
OpenQASM 3.0:
// Assignment syntax
c[0] = measure q[0];
// Measure all
c = measure q;
// Mid-circuit measurement (not possible in 2.0)
bit mid = measure q[0];
h q[1];
if (mid == 1) { x q[1]; }
The arrow syntax (->) is replaced by assignment syntax (=). More importantly, OpenQASM 3 allows measurements anywhere in the circuit, not just at the end.
Quick Reference Table
| Feature | OpenQASM 2.0 | OpenQASM 3.0 |
|---|---|---|
| Version | OPENQASM 2.0; | OPENQASM 3.0; |
| Std library | include "qelib1.inc"; | include "stdgates.inc"; |
| Qubits | qreg q[3]; | qubit[3] q; |
| Bits | creg c[3]; | bit[3] c; |
| Measure | measure q -> c; | c = measure q; |
| Condition | if(c==1) x q[0]; | if (c[0] == 1) { x q[0]; } |
| Loops | Not available | for, while |
| Timing | Not available | delay, stretch, box |
| Pulses | Not available | defcal, cal |
Common Mistakes
These are the most frequent errors encountered when writing OpenQASM 3 programs.
1. Wrong Version String Format
// WRONG: missing .0
OPENQASM 3;
// CORRECT: include the minor version
OPENQASM 3.0;
The parser requires 3.0, not 3. This applies to all version declarations; OPENQASM 2.0 also requires the .0.
2. Missing Semicolons
// WRONG: missing semicolons after declarations and statements
qubit[3] q
bit[3] c
h q[0]
// CORRECT
qubit[3] q;
bit[3] c;
h q[0];
Every declaration and statement must end with a semicolon. Control flow bodies (if, for, while) use braces and do not need a semicolon after the closing brace, but the statements inside them do.
3. Using float Where angle Is More Appropriate
// SUBOPTIMAL: float for rotation angles in a feedback loop
float[64] theta = 0.0;
bit b;
b = measure q[0];
if (b == 1) {
theta = theta + 0.7853981633974483; // pi/4, but in float representation
}
rz(theta) q[1];
// BETTER: angle type wraps automatically and uses fixed-point arithmetic
angle[20] theta = 0;
bit b;
b = measure q[0];
if (b == 1) {
theta = theta + pi / 4; // Clean, wraps modulo 2*pi automatically
}
rz(theta) q[1];
The float[64] type requires a floating-point unit on the classical controller, which adds latency. The angle[20] type uses fixed-point arithmetic that maps directly to efficient hardware operations. Use float only when you need dynamic range beyond or when performing non-angular calculations.
4. Confusing Qubit Arrays with Individual Qubits
// This declares an array of 3 qubits
qubit[3] q;
// This declares a single qubit (NOT a 1-element array)
qubit r;
// WRONG: indexing a single qubit
// r[0]; // Error: r is not an array
// WRONG: passing an array where a single qubit is expected
// h q; // Error if h is defined for a single qubit, this passes the whole register
// CORRECT: index into the array
h q[0];
h q[1];
// CORRECT: apply to individual qubit
h r;
// CORRECT: some operations accept entire registers
bit[3] c = measure q; // Measures all three qubits
5. Forgetting That Mid-Circuit Measurements Collapse State
qubit[2] q;
bit b;
// Prepare Bell state
h q[0];
cx q[0], q[1];
// MISTAKE: measuring q[0] collapses the Bell state
b = measure q[0];
// After this measurement, q[0] is in |0> or |1> (classical state)
// The entanglement between q[0] and q[1] is destroyed
// q[1] is now also in a definite state (|0> or |1>), correlated with b
// If you wanted to use the entanglement later, the measurement destroyed it.
// You must apply corrections (as in teleportation) or restructure the circuit.
// CORRECT approach if you need the entanglement:
// Either measure later, or use the measurement result to correct
if (b == 1) {
x q[1]; // Conditional correction preserves useful information
}
This is especially important in error correction circuits where syndrome measurements must not disturb the encoded data. Always use ancilla qubits for syndrome extraction rather than measuring data qubits directly.
6. Misusing stretch vs delay
qubit[2] q;
// WRONG: using stretch where you need a fixed delay
// stretch is resolved by the compiler; you cannot guarantee its value
stretch s;
delay[s] q[0]; // The compiler might set s to 0ns or 500ns
// CORRECT: use a fixed duration when you need precise timing
delay[200ns] q[0];
// stretch is appropriate when you want the compiler to insert padding
// to align operations across qubits:
stretch padding;
delay[padding] q[0]; // Compiler adjusts to align with q[1]'s schedule
cx q[0], q[1]; // Both qubits arrive at the gate at the same time
// WRONG: using delay for alignment when stretch is more appropriate
delay[100ns] q[0]; // This hardcodes a value that might not align correctly
// on different hardware or after transpilation changes
cx q[0], q[1];
Use delay[Tns] when you need an exact, known idle time (dynamical decoupling, T1/T2 characterization). Use stretch when you want the compiler to determine the correct padding to align operations across qubits.
OpenQASM 3 represents a maturation of quantum assembly language from a simple circuit notation into a genuine programming language for quantum-classical systems. Its timing primitives, real-time classical control, and pulse-level access reflect the practical demands of running useful algorithms on real hardware today.
Was this tutorial helpful?