Amazon Braket Advanced Free 9/11 in series 40 min

Pulse-Level Quantum Control with Amazon Braket

Program quantum hardware at the pulse level using Amazon Braket's OpenPulse interface. Define custom gate sequences and explore native hardware calibrations.

What you'll learn

  • Amazon Braket
  • OpenPulse
  • pulse control
  • native gates
  • hardware calibration

Prerequisites

  • Strong Python skills
  • Solid quantum computing foundations
  • Linear algebra and complex numbers

Overview

Every quantum gate you write in a circuit is ultimately implemented as a precisely shaped microwave pulse applied to a qubit. When you write circuit.h(0) in Braket, the hardware executes a pre-calibrated sequence of microwave pulses that the hardware vendor has designed and benchmarked to implement a Hadamard rotation. Gate-level programming abstracts away these physical signals entirely, which is convenient for algorithm development but limiting for hardware-level experiments.

Pulse-level control exposes the underlying microwave signals directly. Instead of asking the hardware to “apply an H gate,” you define the exact waveform shape, amplitude, duration, and carrier frequency of the microwave signal that drives the qubit. This bypasses the vendor’s pre-calibrated gate sequences and gives you full control over the physical operation.

This level of access is useful for several categories of work:

  • Custom gate calibration: optimizing pulse shapes for lower error rates on your specific qubits, since each physical qubit has slightly different properties and the vendor’s one-size-fits-all calibrations are not always optimal
  • Cross-resonance experiments: probing the coupling strength between qubits by modulating the drive frequency of one qubit at the resonance frequency of a neighboring qubit
  • Novel gate families: implementing gates that are not in the standard gate set, such as fractional Toffoli gates, custom entanglers, or parametric gates with continuous rotation angles defined at the pulse level
  • Noise characterization: measuring T1 and T2 relaxation times using inversion recovery and Ramsey sequences, which require precise control over pulse timing and phase

Pulse programming is traditionally the domain of experimental quantum physicists working directly with laboratory equipment. Amazon Braket’s OpenPulse interface makes it accessible from Python, allowing you to define and execute pulse-level experiments on cloud-connected superconducting hardware without owning a dilution refrigerator.

Prerequisites

You need an Amazon Braket account and access to a device that supports OpenPulse (currently select Rigetti devices). Install the SDK:

pip install amazon-braket-sdk amazon-braket-default-simulator
from braket.aws import AwsDevice
from braket.pulse import PulseSequence, Frame, Port
from braket.circuits import Circuit

How Superconducting Qubits Work

Before diving into pulse programming, it helps to understand the physical system you are controlling.

Superconducting qubits are artificial atoms fabricated from superconducting circuits that contain Josephson junctions. Like natural atoms, they have discrete energy levels. The two lowest energy levels, labeled |0> and |1>, serve as the computational basis states of the qubit. Higher energy levels (|2>, |3>, and so on) also exist but are ideally never populated during computation.

The transition frequency between |0> and |1> is typically in the 4 to 8 GHz range, which falls in the microwave portion of the electromagnetic spectrum. To manipulate the qubit state, you apply a shaped microwave pulse at this resonance frequency through a coaxial cable connected to the qubit’s control port.

The shape, duration, and amplitude of the pulse determine which rotation the qubit undergoes on the Bloch sphere:

  • A pi pulse (180-degree rotation around the X axis) flips |0> to |1> and vice versa. This is the X gate.
  • A pi/2 pulse (90-degree rotation around the X axis) creates an equal superposition. This is the SX (square root of X) gate.
  • A pi/2 pulse with a 90-degree phase offset rotates around the Y axis instead of X, producing the SY gate.
  • The Hadamard gate is implemented as a combination of rotations: a pi/2 rotation around Y followed by a pi rotation around X (or equivalent decompositions, depending on the vendor’s calibration).

The phase of the microwave carrier determines which axis on the Bloch sphere the rotation occurs around. The amplitude determines the rotation angle. The duration sets the total rotation for a given amplitude. These three parameters (phase, amplitude, duration) give you complete control over arbitrary single-qubit rotations.

Two-qubit gates use different mechanisms. In Rigetti’s architecture, two-qubit gates typically use parametric coupling or cross-resonance techniques, where microwave signals are applied at frequencies related to the coupling between neighboring qubits.

Understanding Frames and Ports

OpenPulse models hardware in terms of two abstractions: ports and frames.

A Port corresponds to a physical microwave channel on the control hardware. Concretely, this is a coaxial cable connected to a specific function on a specific qubit. Each qubit typically has:

  • A drive port for single-qubit gate pulses (the microwave signal that rotates the qubit)
  • A readout port for measurement pulses (sends a probe signal to a resonator coupled to the qubit)
  • One or more coupling ports for two-qubit interactions (on architectures that use flux-tunable couplers)

The dt parameter in the Port definition specifies the sampling interval of the control electronics. A value of dt=1e-9 (1 nanosecond) means the arbitrary waveform generator (AWG) outputs one sample every nanosecond, giving a 1 GS/s sampling rate. All pulse durations and waveform lengths must be integer multiples of this sampling interval.

A Frame is defined by a carrier frequency and a phase. It describes the oscillating reference wave that your pulse envelope is modulated onto. When you “play” a waveform on a frame, the hardware multiplies your envelope by a cosine wave at the frame’s carrier frequency and phase, then sends the resulting signal out through the port.

The carrier frequency must match the qubit’s transition frequency for the pulse to be resonant. If the frequency is detuned from resonance, the pulse produces a rotation around a tilted axis on the Bloch sphere, which is usually undesirable for standard gates but useful for certain characterization experiments.

# Inspect available frames on a device (requires hardware access)
# device = AwsDevice("arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2")
# gate_model_props = device.properties.paradigm
# print(gate_model_props)

# For local exploration, define frames manually
drive_frame = Frame(
    frame_id="drive_q0",
    port=Port(port_id="q0_drive", dt=1e-9, properties={}),
    frequency=5.0e9,     # 5 GHz carrier (must match qubit frequency)
    phase=0.0,           # initial phase of the carrier wave
    is_predefined=False,
)

When working with real hardware, you do not define frames manually. Instead, you retrieve the predefined frames from the device properties. Each device publishes its available frames with the correct frequencies already set based on the latest calibration data.

# On real hardware, retrieve predefined frames:
# device = AwsDevice("arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2")
# frames = device.frames
# drive_frame_q0 = frames["q0_rf_frame"]  # predefined drive frame for qubit 0
# readout_frame_q0 = frames["q0_ro_rx_frame"]  # predefined readout frame

Waveform Shapes and What They Do

The shape of a pulse envelope determines both the quality of the gate operation and which unwanted transitions it might excite. Braket provides several built-in waveform classes.

Gaussian

A Gaussian pulse has a smooth bell-curve shape that minimizes spectral leakage. In frequency space, a Gaussian in time is also a Gaussian, which means the pulse energy is concentrated near the carrier frequency with minimal power at neighboring frequencies. This reduces unwanted excitation of the |1> to |2> transition (which is typically 200 to 300 MHz below the |0> to |1> transition in transmon qubits).

Gaussian pulses are the standard choice for single-qubit gates on most superconducting platforms.

from braket.pulse.waveforms import GaussianWaveform

gaussian_wf = GaussianWaveform(
    id="gaussian_example",
    length=100e-9,       # 100 ns total duration
    sigma=25e-9,         # standard deviation of the Gaussian envelope
    amplitude=0.5,       # peak amplitude (normalized, 0 to 1)
    zero_at_edges=True,  # truncate and re-normalize so edges are zero
)

The sigma parameter controls the width. A smaller sigma produces a shorter effective pulse but with more spectral leakage. A sigma of length/4 is a common starting point.

DRAG (Derivative Removal via Adiabatic Gate)

DRAG pulses add a derivative correction on the quadrature (Y) channel to suppress leakage to the |2> state. The in-phase (X) channel carries a Gaussian envelope while the quadrature channel carries a scaled derivative of that Gaussian. This correction is critical for achieving gate fidelities above 99.5% on transmon qubits.

from braket.pulse.waveforms import DragGaussianWaveform

drag_wf = DragGaussianWaveform(
    id="drag_example",
    length=100e-9,       # 100 ns total duration
    sigma=25e-9,         # standard deviation
    amplitude=0.5,       # peak amplitude
    beta=0.2,            # DRAG correction coefficient
    zero_at_edges=True,
)

The beta parameter controls the strength of the derivative correction. Its optimal value depends on the anharmonicity of the specific qubit (the frequency difference between the |0>-|1> and |1>-|2> transitions). Typical values range from 0.1 to 2.0 and must be calibrated experimentally for each qubit.

Constant (Square/Rectangular)

A constant waveform has a flat amplitude for its entire duration. Square pulses are simple but have poor spectral properties: the sharp edges in time produce a sinc function in frequency space, spreading power across a wide bandwidth. This makes them unsuitable for high-fidelity single-qubit gates.

However, square pulses are commonly used for cross-resonance two-qubit gates, where a long, flat pulse is applied to one qubit at the resonance frequency of a neighboring qubit. The duration of the flat-top portion controls the entangling angle.

from braket.pulse.waveforms import ConstantWaveform

square_wf = ConstantWaveform(
    id="square_example",
    length=200e-9,       # 200 ns duration
    iq=0.3,              # constant amplitude (can be complex for IQ control)
)

Choosing a Waveform

For single-qubit gates, start with DRAG Gaussian pulses. They provide the best fidelity on transmon hardware. Use plain Gaussian pulses if DRAG calibration is not yet available for your target qubit. Reserve constant waveforms for two-qubit cross-resonance experiments or for calibration sequences where simplicity is more important than fidelity.

Building a Pulse Sequence

A PulseSequence chains waveform operations on frames. Use play to emit a waveform and shift_phase or set_frequency to control the carrier.

from braket.pulse.waveforms import GaussianWaveform

# Gaussian pulse for a pi rotation (X gate equivalent)
waveform = GaussianWaveform(
    id="pi_pulse",
    length=100e-9,     # 100 ns
    sigma=25e-9,       # 25 ns sigma
    amplitude=0.5,
    zero_at_edges=True,
)

pulse_seq = (
    PulseSequence()
    .play(drive_frame, waveform)
    .shift_phase(drive_frame, 1.5707963)   # pi/2 phase shift
)

print(pulse_seq)

The shift_phase operation rotates the frame’s reference phase by the specified angle in radians. This changes the axis of rotation for subsequent pulses without requiring a physical delay. Phase shifts are instantaneous in the control hardware since they only modify the digital oscillator generating the carrier signal.

Other operations available in a PulseSequence include:

  • set_frequency(frame, frequency): change the carrier frequency mid-sequence (useful for frequency-swept experiments)
  • delay(frame, duration): insert a wait period (used in T1 and T2 characterization sequences)
  • barrier(frames): synchronize the timing across multiple frames before proceeding
  • capture_v0(frame): trigger a measurement acquisition on a readout frame

Simple Qubit Rotation Experiment

This example implements an RX(theta) rotation using a Gaussian pulse and sweeps the pulse amplitude to observe Rabi oscillations. Rabi oscillations are the periodic flopping between |0> and |1> as the drive amplitude increases, providing the most basic validation that your pulse is resonant and your qubit is responding.

import numpy as np
from braket.pulse import PulseSequence, Frame, Port
from braket.pulse.waveforms import GaussianWaveform

# Define the drive frame for qubit 0
# On real hardware, use device.frames["q0_rf_frame"] instead
drive_frame = Frame(
    frame_id="drive_q0",
    port=Port(port_id="q0_drive", dt=1e-9, properties={}),
    frequency=5.0e9,
    phase=0.0,
    is_predefined=False,
)

def make_rabi_pulse(amplitude: float) -> PulseSequence:
    """Create a pulse sequence with a Gaussian at the given amplitude."""
    wf = GaussianWaveform(
        id=f"rabi_amp_{amplitude:.4f}",
        length=100e-9,
        sigma=25e-9,
        amplitude=amplitude,
        zero_at_edges=True,
    )
    return PulseSequence().play(drive_frame, wf)

# Sweep amplitudes from 0 to 1
amplitudes = np.linspace(0.0, 1.0, 51)

# On real hardware, you would submit each pulse sequence as a task:
# device = AwsDevice("arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2")
# results = []
# for amp in amplitudes:
#     ps = make_rabi_pulse(amp)
#     task = device.run(ps, shots=1024)
#     result = task.result()
#     prob_1 = result.measurement_counts.get("1", 0) / 1024
#     results.append(prob_1)

# Expected result: sinusoidal oscillation of P(|1>) vs amplitude
# The amplitude at which P(|1>) first reaches 1.0 is your pi-pulse amplitude
# Half that amplitude gives your pi/2 pulse

# Simulated expected output for illustration
expected_prob_1 = np.sin(np.pi * amplitudes / 0.5) ** 2  # assuming pi pulse at amp=0.5
expected_prob_1 = np.clip(expected_prob_1, 0, 1)

# Plot with matplotlib
# import matplotlib.pyplot as plt
# plt.figure(figsize=(8, 4))
# plt.plot(amplitudes, expected_prob_1, 'b-', label="P(|1>)")
# plt.xlabel("Pulse amplitude (arb. units)")
# plt.ylabel("Probability of |1>")
# plt.title("Rabi Oscillation")
# plt.legend()
# plt.grid(True)
# plt.show()

print("Rabi sweep defined for", len(amplitudes), "amplitude points")

The key signature of a successful Rabi experiment is a clean sinusoidal oscillation. The amplitude at which the probability of measuring |1> first reaches 1.0 is your calibrated pi-pulse amplitude. Half that value gives the pi/2 pulse amplitude. These two values are the foundation for all subsequent gate calibrations on that qubit.

If the oscillation is damped, decays, or shows irregular features, common causes include: detuned carrier frequency, qubit relaxation during long pulse sequences, or leakage to the |2> state at high amplitudes.

Embedding Pulses in a Gate Circuit

Braket allows mixing gate-level operations with pulse blocks in a single circuit. Use Circuit.add_verbatim_box alongside pulse sequences for hybrid gate/pulse programs.

from braket.circuits import Circuit
from braket.pulse import PulseSequence

def make_custom_rx(angle: float, drive_frame: Frame) -> PulseSequence:
    """Approximate RX(angle) via a scaled Gaussian pulse."""
    amp = angle / (2 * 3.14159)   # linear amplitude scaling
    wf = GaussianWaveform(
        id=f"rx_{angle:.3f}",
        length=100e-9,
        sigma=25e-9,
        amplitude=amp,
        zero_at_edges=True,
    )
    return PulseSequence().play(drive_frame, wf)

# Build a circuit that uses a pulse block for qubit 0
circuit = Circuit()
circuit.h(1)
circuit.cnot(1, 0)
# circuit.add_pulse_gate(make_custom_rx(3.14159, drive_frame), target=0)
print(circuit)

This hybrid approach is useful when you want to customize one specific gate while relying on the vendor’s calibrated implementations for everything else. For example, you might define a custom high-fidelity single-qubit rotation at the pulse level while using the standard CNOT implementation for two-qubit entanglement.

Exploring Calibration Data

Hardware devices publish default calibration parameters that are updated regularly (typically every few hours). Pulling these values helps you understand the baseline pulse shapes the device vendor uses and informs your own pulse designs.

# With a real device:
# device = AwsDevice("arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2")
# calibration = device.properties.provider
# print(calibration)

# Typical fields you will find:
calibration_fields = [
    "qubit_frequencies",     # drive frequencies per qubit
    "t1_times",              # qubit relaxation times
    "t2_times",              # qubit dephasing times
    "gate_fidelities",       # benchmarked gate fidelities
    "native_gate_durations", # duration of each native gate in ns
]
for field in calibration_fields:
    print(f"  {field}")

Here is what each field tells you and how to use it:

qubit_frequencies: The resonant frequency of each qubit in Hz (typically 4 to 8 GHz). Your drive pulses must be tuned to this frequency for the pulse to be resonant. Even a few MHz of detuning produces off-axis rotations that reduce gate fidelity. Always read the latest calibration data before running pulse experiments, since qubit frequencies drift over time due to two-level system (TLS) defects and thermal fluctuations.

t1_times: The energy relaxation time (T1) for each qubit, measured in microseconds. T1 sets the timescale over which the qubit spontaneously decays from |1> to |0>. If your total circuit execution time approaches or exceeds T1, decoherence dominates and your results become unreliable. Typical T1 values on current hardware range from 20 to 100 microseconds. Divide your circuit depth (total gate time) by T1 to estimate how much relaxation error to expect.

t2_times: The dephasing time (T2) for each qubit, also in microseconds. T2 governs how quickly the phase coherence between |0> and |1> is lost. T2 is always less than or equal to 2*T1. Ramsey experiments measure T2 directly by applying two pi/2 pulses separated by a variable delay and observing the oscillation decay. T2 sets the practical limit on how long you can maintain superposition states.

gate_fidelities: Benchmarked error rates per gate, measured by the vendor using randomized benchmarking or cross-entropy benchmarking. Fidelities are reported as decimal values where 1.0 is perfect. A single-qubit gate fidelity of 0.999 means a 0.1% error per gate. Two-qubit gate fidelities are typically lower (0.95 to 0.99). Use these values to estimate total circuit error: for n gates each with fidelity f, the approximate circuit fidelity is f^n.

native_gate_durations: How long each standard gate takes in nanoseconds. Single-qubit gates are typically 20 to 100 ns; two-qubit gates range from 100 to 500 ns. These durations are critical for calculating whether your total circuit execution time stays within the T2 coherence window. Shorter gates allow deeper circuits before decoherence becomes dominant.

Measuring Pulse Output with a Local Simulator

The default Braket local simulator does not support pulse execution, but you can validate pulse sequence construction before sending to hardware.

from braket.devices import LocalSimulator

# Gate-level circuit for local validation
validation_circuit = Circuit().h(0).cnot(0, 1).probability(target=[0, 1])
device = LocalSimulator()
task = device.run(validation_circuit, shots=1024)
result = task.result()
print(result.measurement_probabilities)

The local simulator is useful for verifying that your circuit structure is correct, testing classical post-processing code, and ensuring your program logic works before spending money on hardware shots. It cannot simulate the actual pulse dynamics (waveform shapes, decoherence, leakage), so you should not rely on it to validate pulse fidelity.

For pulse-level simulation with realistic physics, consider using QuTiP (Quantum Toolbox in Python) to numerically solve the time-dependent Schrodinger equation for your pulse waveform acting on a multi-level transmon model. This gives you a preview of gate fidelity and leakage before committing to hardware time.

When to Use Pulse Control

Pulse-level programming is a powerful but specialized tool. Here is when it makes sense and when it does not.

Use pulse control for:

  • Custom gate calibration: if you need higher fidelity than the vendor’s default calibration provides, or if specific qubits in your circuit are underperforming, you can recalibrate individual gates at the pulse level
  • Gates not in the standard set: if your algorithm requires a gate that is not natively supported (fractional rotations, custom entanglers, continuously parameterized two-qubit gates), define it directly as a pulse sequence
  • Qubit characterization: measuring T1, T2, qubit frequency, anharmonicity, and coupling strengths requires pulse-level access for inversion recovery, Ramsey, and spectroscopy experiments
  • Research: developing new gate protocols, studying qubit physics, benchmarking novel error mitigation strategies, or exploring optimal control techniques

Do not use pulse control for:

  • Production algorithm execution: for VQE, QAOA, Grover’s search, or any standard algorithm, use gate-level programming. The vendor’s calibrated gates are extensively benchmarked and optimized. Rolling your own pulses for standard gates almost always produces worse results unless you are specifically investing time in per-qubit calibration.
  • Hardware you have not characterized: always run calibration experiments first to determine the correct pulse amplitudes and frequencies for your target qubits before attempting complex pulse sequences

Platform availability: Pulse control on Braket is currently available only on Rigetti devices. Other providers (IonQ, IQM) do not expose pulse-level access through Braket. Check the Braket documentation for the latest device support.

Common Mistakes

Pulse-level programming has many failure modes that do not exist at the gate level. Here are the most common pitfalls.

Setting pulse amplitude too high. Transmon qubits have a |2> state that sits roughly 200 to 300 MHz below the |0>-|1> transition frequency (this gap is called the anharmonicity). If your pulse amplitude is too large, you drive population into |2>, which is outside the computational subspace and produces nonsensical results. Start with low amplitudes and increase gradually while monitoring the Rabi oscillation shape. Deviations from a clean sinusoid indicate leakage.

Incorrect carrier frequency. If the carrier frequency of your frame does not match the qubit’s resonance frequency, the pulse produces rotations around a tilted axis on the Bloch sphere. Small detunings (a few MHz) reduce gate fidelity. Large detunings (tens of MHz or more) make the pulse almost completely ineffective. Always read the latest calibration data to get the current qubit frequency before defining your frames.

Ignoring pulse duration constraints. Real hardware has minimum and maximum pulse lengths imposed by the control electronics. Minimum pulse lengths are typically 4 to 8 ns (set by the AWG sampling rate and bandwidth). Maximum pulse lengths are bounded by qubit coherence times. Additionally, pulse lengths must be integer multiples of the sampling interval dt. A pulse sequence that violates these constraints will be rejected by the hardware.

Not validating with gate-level circuits first. Before implementing an algorithm at the pulse level, build and test the same algorithm using standard gates. This confirms that your algorithm logic, measurement, and classical post-processing are correct. Only then translate specific gates to pulse-level implementations. Debugging algorithm errors and pulse calibration errors simultaneously is extremely difficult.

Assuming the local simulator validates pulses. The Braket local simulator executes gate-level circuits only. It does not simulate pulse dynamics, decoherence, or leakage. A pulse sequence that “runs” on the local simulator has only been validated for syntactic correctness, not physical correctness. Use the simulator for circuit structure validation and classical post-processing testing, but do not treat simulator results as predictions of hardware behavior for pulse programs.

Neglecting frame synchronization. When operating on multiple qubits simultaneously, the timing relationship between frames matters. Use barrier operations to explicitly synchronize frames before time-critical operations like two-qubit gates or correlated measurements. Unsynchronized frames can produce phase errors that are difficult to diagnose.

Costs and Scheduling

Pulse programs count toward device shot costs just like gate circuits. Each shot executes the full pulse sequence once. Plan your experiments:

# Estimate shot cost before running
shots = 2048
cost_per_shot = 0.00035   # approximate Rigetti cost in USD
print(f"Estimated cost: ${shots * cost_per_shot:.4f}")

For calibration sweeps that require many parameter points (like the Rabi experiment above with 51 amplitude values), the total cost adds up. A 51-point sweep at 1024 shots per point costs roughly 51 * 1024 * 0.00035=0.00035 = 18.28. Budget accordingly and consider reducing the number of shots for initial exploratory sweeps (256 shots is often sufficient to see the oscillation shape), then increasing shots for final high-precision calibration runs.

Rigetti devices have scheduled availability windows. Check the Braket console for the current device schedule before queuing pulse experiments. Jobs submitted outside the availability window sit in the queue until the device comes online, which can mean waiting hours or days.

Summary

OpenPulse gives you direct access to the microwave signals that drive superconducting qubits. You define waveform envelopes (Gaussian, DRAG, or constant), modulate them onto carrier frames tuned to the qubit resonance frequency, and send them through physical ports connected to the control hardware.

The workflow for pulse-level experiments follows a consistent pattern: retrieve calibration data from the device, define frames at the correct qubit frequencies, build pulse sequences with appropriate waveform shapes, validate circuit structure locally, then execute on hardware and analyze measurement results.

Start with simple characterization experiments (Rabi oscillations, T1 measurement) to build confidence in your pulse parameters before attempting complex multi-qubit sequences. Use gate-level programming for standard algorithms and reserve pulse control for the cases where you genuinely need access to the underlying physics.

Was this tutorial helpful?