Overview

OpenPulse extends OpenQASM3 beyond the gate model by introducing constructs for ports, frames, and waveforms that describe the actual control signals sent to quantum hardware. This allows programmers to implement custom calibrated gates, adjust frequencies and phases dynamically, and align timing at the hardware level. With OpenPulse, quantum programs can bridge the gap between high-level circuits and the physical pulses that drive qubit operations.

PyQASM 🤝 OpenPulse

PyQASM now understands and validates OpenPulse constructs embedded in OpenQASM3 programs. This includes parsing cal blocks and defcal definitions and validating pulse-level statements. While most existing tools focus on gate-level OpenQASM, few provide strong support for pulse-level semantics and most of them lack comprehensive semantic analysis — such as validating scope rules, deterministic durations in defcal, and hardware-level constraints

PyQASM aims to fill this gap by:

  • Providing a unified parser and validator for both OpenQASM3 and OpenPulse.

  • Enforcing semantic correctness of pulse operations, not just syntax.

  • Allowing users to configure backend-specific rules (duration tracking, frame limits, etc.).

  • Building towards a complete analysis and compilation toolkit for pulse-level programs, bridging high-level circuits with low-level control.

This makes PyQASM a foundation for researchers and hardware providers who need validation and compilation of OpenPulse programs.

Core Constructs

The OpenPulse grammar—activated by defcal grammar "openpulse"; — adds these core constructs to OpenQASM3.

  • Ports (port): Abstract I/O channels representing hardware control lines.

    • Real hardware often restricts the number of active frames per physical port. pyqasm introduces a frame_limit_per_port option that users can set when loading a program.
    QASM-Code
    import pyqasm
    
    qasm = """
    OPENQASM 3.0;
    defcalgrammar "openpulse";
    cal {
         port d0;
         frame fr1 = newframe(d0, 100.0, 5.0);
         frame fr2 = newframe(d0, 100.0, 5.0);
    }
    """
    module = pyqasm.loads(qasm, frame_limit_per_port = 1)
    module.unroll()
    print(pyqasm.dumps(module))
    output
    ERROR:pyqasm: Error at line 4, column 16 in QASM file
    
    > > > > > > newframe(d0, 100.0, 5.0)
    
    pyqasm.exceptions.ValidationError: Port 'd0' has exceeded the 'frame-limit' of '1'
    
  • Frames (frame): Stateful carriers with port, timing, frequency, and phase.

    • Users can configure whether frames are allowed in defcal blocks via the frame_in_defcal option when loading a program.
    QASM-Code
    import pyqasm
    
    qasm = """
    OPENQASM 3.0;
    defcalgrammar "openpulse";
    cal {
         port d0;
         frame fr1 = newframe(d0, 100.0, 5.0);
    }
    defcal gate $0{
         frame fr2 = newframe(d0, 100.0, 5.0);
    }
    """
    
    module = pyqasm.loads(qasm, frame_in_defcal = False)
    module.unroll()
    print(pyqasm.dumps(module))
    output
    ERROR:pyqasm: Error at line 2, column 16 in QASM file
    
    > > > > > > newframe(d0, 100.0, 5.0)
    
    pyqasm.exceptions.ValidationError: 'Frame' initialization in 'defcal' block is not allowed
    
  • pyqasm also supports the full set of frame manipulators defined in the OpenPulse grammar

    • set_frequency() / shift_frequency() / shift_phase() / get_frequency() / get_phase().
  • Waveforms (waveform): Envelopes modulating signals on ports.

    • pyqasm supports all waveform types defined in the OpenPulse specification — gaussian, sech, gaussian_square, drag, constant, sine, mix, sum, phase_shift, scale.
    • validates all declarations conform to the OpenPulse grammar, including parameter correctness and waveform compatibility.
  • Capture (capture): Measurement/capture operations.

    • pyqasm also supports the capture operations defined in the OpenPulse grammar — capture_v1(), capture_v2(), capture_v3(), capture_v4().
  • Operations within cal / defcal:

    • play() — apply a waveform via a frame.

      • By default, play is intended for use inside defcal blocks. pyqasm introduces a configurable option, play_in_cal_block, which allows users to enable or restrict the use of play instructions inside cal blocks as well.
      QASM-Code
      import pyqasm
      
      qasm = """
      OPENQASM 3.0;
      defcalgrammar "openpulse";
      cal {
         port d0;
         frame fr1 = newframe(d0, 100.0, 5.0);
         waveform wf = constant(1.0+2.0im, 100.0ns);
         play(fr1, wf);
      }
      """
      module = pyqasm.loads(qasm, play_in_cal_block = False)
      module.unroll()
      print(pyqasm.dumps(module))
      output
      ERROR:pyqasm: Error at line 5, column 11 in QASM file
      
      > > > > > > play(fr1, wf)
      
      pyqasm.exceptions.ValidationError: 'Play' function is only allowed in 'defcal' block
      
    • delay() — Advance the time-cursor of a frame by a given duration.

    • barrier() — synchronize the time-cursor of multiple frames to the same point in time.

Capabilities

  • Duration Tracking

    • Each frame carries an internal time-cursor that advances as operations (play, delay, capture) are applied.
    • pyqasm performs semantic tracking of durations, ensuring that timing across frames remains consistent throughout the program.
    • This enables correct validation of defcal blocks, which must have deterministic total durations across all execution paths.
  • PyQASM Supports Frames-Collisions validation in defcal blocks.

  • PyQASM also unrolls cal blocks, which is useful for debugging and visualization. Since, defcal are acted as functions, we are not unrolling them.

  • Currently, pyqasm does not support extern declarations in OpenPulse blocks.

For more details, please refer to the OpenPulse specification.

Examples

QASM-Code
import pyqasm
qasm_code = """
OPENQASM 3.0;
defcalgrammar "openpulse";

complex[float[32]] amp = 1.0 + 2.0im;
cal {
   port d0;
   frame driveframe = newframe(d0, 5.0e9, 0.0);
   waveform wf = gaussian(amp, 16ns, 4ns);
}

const float frequency_start = 4.5e9;
const float frequency_step = 1e6;
const int frequency_num_steps = 3;

defcal saturation_pulse $0 {
   play(driveframe, constant(amp, 100e-6s));
}

cal {
   set_frequency(driveframe, frequency_start);
}

for int i in [1:frequency_num_steps] {
   cal {
       shift_frequency(driveframe, frequency_step);
   }
   saturation_pulse $0;
}
"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))
output
OPENQASM 3.0;
qubit[1] __PYQASM_QUBITS__;
defcalgrammar "openpulse";
cal {
 port d0;
 frame driveframe = newframe(d0, 5000000000.0, 0.0, 0ns);
 waveform wf = gaussian(1.0 + 2.0im, 16.0ns, 4.0ns);
}
defcal saturation_pulse() $0 {
 play(driveframe, constant(amp, 0.0001s));
}
cal {
 set_frequency(driveframe, 4500000000.0);
}
cal {
 shift_frequency(driveframe, 4501000000.0);
}
saturation_pulse __PYQASM_QUBITS__[0];
cal {
 shift_frequency(driveframe, 4502000000.0);
}
saturation_pulse __PYQASM_QUBITS__[0];
cal {
 shift_frequency(driveframe, 4503000000.0);
}
saturation_pulse __PYQASM_QUBITS__[0];
QASM-Code
import pyqasm
qasm_code = """
OPENQASM 3.0;
defcalgrammar "openpulse";
qubit[4] q;
const duration pulse_length_start = 20ns;
const duration pulse_length_step = 1ns;
const int pulse_length_num_steps = 3;
cal {
   port d0;
   frame driveframe = newframe(d0, 5.0e9, 0.0);
}
for int i in [1:pulse_length_num_steps] {
    duration pulse_length = pulse_length_start + (i-1)*pulse_length_step;
    duration sigma = pulse_length / 4;
    // since we are manipulating pulse lengths it is easier to define and play the waveform in a `cal` block
    cal {
        waveform wf = gaussian(1.0 + 2.0im, pulse_length, sigma);
        // assume frame can be linked from a vendor supplied `cal` block
        play(driveframe, wf);
    }
    measure $0;
}
"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))
output
OPENQASM 3.0;
qubit[4] __PYQASM_QUBITS__;
defcalgrammar "openpulse";
cal {
 port d0;
 frame driveframe = newframe(d0, 5000000000.0, 0.0, 0ns);
}
cal {
 waveform wf = gaussian(1.0 + 2.0im, 20.0ns, 5.0ns);
 play(driveframe, wf);
}
measure __PYQASM_QUBITS__[0];
cal {
 waveform wf = gaussian(1.0 + 2.0im, 21.0ns, 5.25ns);
 play(driveframe, wf);
}
measure __PYQASM_QUBITS__[0];
cal {
 waveform wf = gaussian(1.0 + 2.0im, 22.0ns, 5.5ns);
 play(driveframe, wf);
}
measure __PYQASM_QUBITS__[0];
QASM-Code
import pyqasm
qasm_code = """
OPENQASM 3.0;
defcalgrammar "openpulse";

// Raman transition detuning delta from the 5S1/2 to 5P1/2 transition
const float delta = 100e6;

// Hyperfine qubit frequency
const float qubit_freq = 6.0e9;

// Positional frequencies for the AODS to target the specific qubit
const float q1_pos_freq = 5.0e9;
const float q2_pos_freq = 5.0e9;
const float q3_pos_freq = 5.0e9;

// Calibrated amplitudes and durations for the Raman pulses supplied via the AOD envelopes
const complex[float[32]] q1*π_half_amp = 1.0 + 2.0im;
const complex[float[32]] q2*π*half_amp = 1.0 + 2.0im;
const complex[float[32]] q3*π_half_amp = 1.0 + 2.0im;
const duration pi_half_time = 10.0ns;

// Time-proportional phase increment
const float tppi_1 = 1.0;
const float tppi_2 = 1.0;
const float tppi_3 = 1.0;

cal {
port eom_a_port;
port eom_b_port;
port aod_port;

    // Define the Raman frames, which are detuned by an amount delta from the  5S1/2 to 5P1/2 transition
    // and offset from each other by the qubit_freq
    frame raman_a_frame = newframe(eom_a_port, delta, 0.0);
    frame raman_b_frame = newframe(eom_b_port, delta-qubit_freq, 0.0);
    const complex[float[32]] raman_a_amp = 1.0 + 2.0im;
    const complex[float[32]] raman_b_amp = 1.0 + 2.0im;

    // Three frames to phase track each qubit's rotating frame of reference at it's frequency
    frame q1_frame = newframe(aod_port, qubit_freq, 0.0);
    frame q2_frame = newframe(aod_port, qubit_freq, 0.0);
    frame q3_frame = newframe(aod_port, qubit_freq, 0.0);

    // Generic gaussian envelope
    waveform pi_half_sig = gaussian(1.0 + 2.0im, pi_half_time, 100ns);

    // Waveforms ultimately supplied to the AODs. We mix our general Gaussian pulse with a sine wave to
    // put a sideband on the outgoing pulse. This helps us target the qubit position while maintainig the
    // desired Rabi rate.
    waveform q1_pi_half_sig = mix(pi_half_sig, sine(q1_π_half_amp, pi_half_time, q1_pos_freq-qubit_freq, 0.0));
    waveform q2_pi_half_sig = mix(pi_half_sig, sine(q2_π_half_amp, pi_half_time, q2_pos_freq-qubit_freq, 0.0));
    waveform q3_pi_half_sig = mix(pi_half_sig, sine(q3_π_half_amp, pi_half_time, q3_pos_freq-qubit_freq, 0.0));

}
// π/2 pulses on all three qubits
defcal rx(angle theta) $1, $2, $3 {
// Simultaneous π/2 pulses
play(raman_a_frame, constant(raman_a_amp, pi_half_time));
play(raman_b_frame, constant(raman_b_amp, pi_half_time));
play(q1_frame, q1_pi_half_sig);
play(q2_frame, q2_pi_half_sig);
play(q3_frame, q3_pi_half_sig);
}
// π/2 pulse on only qubit $2
defcal rx(angle theta) $2 {
play(raman_a_frame, constant(raman_a_amp, pi_half_time));
play(raman_b_frame, constant(raman_b_amp, pi_half_time));
play(q2_frame, q2_pi_half_sig);
}
// Ramsey sequence on qubit 1 and 3, Hahn echo on qubit 2
for duration tau_val in [1us:1us:2us] {

    // First π/2 pulse
    rx(pi/2) $1, $2, $3;
    // First half of evolution time
    cal {
        delay[tau_val/2] raman_a_frame, raman_b_frame, q1_frame, q2_frame, q3_frame;
    }
    // Hahn echo π pulse composed of two π/2 pulses
    for int ct in [0:1]{
        rx(π/2) $2;
    }
    cal {
        // Align all frames
        barrier raman_a_frame, raman_b_frame, q1_frame, q2_frame, q3_frame;

        // Second half of evolution time
        delay[tau_val/2] raman_a_frame, raman_b_frame, q1_frame, q2_frame, q3_frame;

        // Time-proportional phase increment signals different amount
        shift_phase(q1_frame, tppi_1 * tau_val);
        shift_phase(q2_frame, tppi_2 * tau_val);
        shift_phase(q3_frame, tppi_3 * tau_val);
    }

    // Second π/2 pulse
    rx(π/2) $1, $2, $3;

}
"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))
output
OPENQASM 3.0;
qubit[4] __PYQASM_QUBITS__;
defcalgrammar "openpulse";
cal {
 port eom_a_port;
 port eom_b_port;
 port aod_port;
 frame raman_a_frame = newframe(eom_a_port, 100000000.0, 0.0, 0ns);
 frame raman_b_frame = newframe(eom_b_port, -5900000000.0, 0.0, 0ns);
 const complex[float[32]] raman_a_amp = 1.0 + 2.0im;
 const complex[float[32]] raman_b_amp = 1.0 + 2.0im;
 frame q1_frame = newframe(aod_port, 6000000000.0, 0.0, 0ns);
 frame q2_frame = newframe(aod_port, 6000000000.0, 0.0, 0ns);
 frame q3_frame = newframe(aod_port, 6000000000.0, 0.0, 0ns);
 waveform pi_half_sig = gaussian(1.0 + 2.0im, 10.0ns, 100.0ns);
 waveform q1_pi_half_sig = mix(pi_half_sig, sine(1.0 + 2.0im, 10.0ns, -1000000000.0, 0.0));
 waveform q2_pi_half_sig = mix(pi_half_sig, sine(1.0 + 2.0im, 10.0ns, -1000000000.0, 0.0));
 waveform q3_pi_half_sig = mix(pi_half_sig, sine(1.0 + 2.0im, 10.0ns, -1000000000.0, 0.0));
}
defcal rx(angle theta) $1, $2, $3 {
 play(raman_a_frame, constant(raman_a_amp, pi_half_time));
 play(raman_b_frame, constant(raman_b_amp, pi_half_time));
 play(q1_frame, q1_pi_half_sig);
 play(q2_frame, q2_pi_half_sig);
 play(q3_frame, q3_pi_half_sig);
}
defcal rx(angle theta) $2 {
 play(raman_a_frame, constant(raman_a_amp, pi_half_time));
 play(raman_b_frame, constant(raman_b_amp, pi_half_time));
 play(q2_frame, q2_pi_half_sig);
}
rx(1.5707963267948966) __PYQASM_QUBITS__[1], __PYQASM_QUBITS__[2], __PYQASM_QUBITS__[3];
cal {
 delay[500.0ns] raman_a_frame, raman_b_frame, q1_frame, q2_frame, q3_frame;
}
rx(1.5707963267948966) __PYQASM_QUBITS__[2];
rx(1.5707963267948966) __PYQASM_QUBITS__[2];
cal {
 barrier raman_a_frame, raman_b_frame, q1_frame, q2_frame, q3_frame;
 delay[500.0ns] raman_a_frame, raman_b_frame, q1_frame, q2_frame, q3_frame;
 shift_phase(q1_frame, 0.9735361584457891);
 shift_phase(q2_frame, 0.9735361584457891);
 shift_phase(q3_frame, 0.9735361584457891);
}
rx(1.5707963267948966) __PYQASM_QUBITS__[1], __PYQASM_QUBITS__[2], __PYQASM_QUBITS__[3];
rx(1.5707963267948966) __PYQASM_QUBITS__[1], __PYQASM_QUBITS__[2], __PYQASM_QUBITS__[3];
cal {
 delay[1000.0ns] raman_a_frame, raman_b_frame, q1_frame, q2_frame, q3_frame;
}
rx(1.5707963267948966) __PYQASM_QUBITS__[2];
rx(1.5707963267948966) __PYQASM_QUBITS__[2];
cal {
 barrier raman_a_frame, raman_b_frame, q1_frame, q2_frame, q3_frame;
 delay[1000.0ns] raman_a_frame, raman_b_frame, q1_frame, q2_frame, q3_frame;
 shift_phase(q1_frame, 2.920608475337346);
 shift_phase(q2_frame, 2.920608475337346);
 shift_phase(q3_frame, 2.920608475337346);
}
rx(1.5707963267948966) __PYQASM_QUBITS__[1], __PYQASM_QUBITS__[2], __PYQASM_QUBITS__[3];
QASM-Code
import pyqasm
qasm_code = """
OPENQASM 3.0;
defcalgrammar "openpulse";

const duration electrical_delay = 100ns;
const float q0_ro_freq = 5.0e9;
const float q1_ro_freq = 6.0e9;

cal {
// the transmission/captures ports are the same for $0 and $1
port ro_tx;
port ro_rx;
// readout stimulus and capture frames of different frequencies
frame q0_stimulus_frame = newframe(ro_tx, q0_ro_freq, 0.0);
frame q0_capture_frame = newframe(ro_rx, q0_ro_freq, 0.0);
frame q1_stimulus_frame = newframe(ro_tx, q1_ro_freq, 0.0);
frame q1_capture_frame = newframe(ro_rx, q1_ro_freq, 0.0);

}
defcal multiplexed_readout_and_capture $0, $1 -> bit[2] {
bit[2] b;
int sairam;
waveform q0_ro_wf = constant(1.0 + 2.0im, 100ns);
waveform q1_ro_wf = constant(1.0 + 2.0im, 100ns);

    // multiplexed readout
    play(q0_stimulus_frame, q0_ro_wf);
    play(q1_stimulus_frame, q1_ro_wf);

    // simple boxcar kernel
    waveform ro_kernel = constant(1.0 + 2.0im, 100ns);
    barrier q0_stimulus_frame, q1_stimulus_frame, q0_capture_frame, q1_capture_frame;
    delay[electrical_delay] q0_capture_frame, q1_capture_frame;
    b[0] = capture_v2(q0_capture_frame, ro_kernel);
    b[1] = capture_v2(q1_capture_frame, ro_kernel);
    return b;

}

multiplexed_readout_and_capture $0, $1;
"""
module = pyqasm.loads(qasm_code)
module.unroll()

print(pyqasm.dumps(module))
output
OPENQASM 3.0;
qubit[2] __PYQASM_QUBITS__;
defcalgrammar "openpulse";
cal {
 port ro_tx;
 port ro_rx;
 frame q0_stimulus_frame = newframe(ro_tx, 5000000000.0, 0.0, 0ns);
 frame q0_capture_frame = newframe(ro_rx, 5000000000.0, 0.0, 0ns);
 frame q1_stimulus_frame = newframe(ro_tx, 6000000000.0, 0.0, 0ns);
 frame q1_capture_frame = newframe(ro_rx, 6000000000.0, 0.0, 0ns);
}
defcal multiplexed_readout_and_capture() $0, $1 -> bit[2] {
 bit[2] b;
 int sairam;
 waveform q0_ro_wf = constant(1.0 + 2.0im, 100.0ns);
 waveform q1_ro_wf = constant(1.0 + 2.0im, 100.0ns);
 play(q0_stimulus_frame, q0_ro_wf);
 play(q1_stimulus_frame, q1_ro_wf);
 waveform ro_kernel = constant(1.0 + 2.0im, 100.0ns);
 barrier q0_stimulus_frame, q1_stimulus_frame, q0_capture_frame, q1_capture_frame;
 delay[electrical_delay] q0_capture_frame, q1_capture_frame;
 b[0] = capture_v2(q0_capture_frame, ro_kernel);
 b[1] = capture_v2(q1_capture_frame, ro_kernel);
 return b;
}
multiplexed_readout_and_capture __PYQASM_QUBITS__[0], __PYQASM_QUBITS__[1];