

# Pulse control on Amazon Braket
(Advanced) Pulse control on Amazon Braket

Pulses are the analog signals that control the qubits in a quantum computer. With certain devices on Amazon Braket, you can access the pulse control feature to submit circuits using pulses. You can access pulse control through the Braket SDK, using OpenQASM 3.0, or directly through the Braket APIs. First, introduce some key concepts for pulse control in Braket.

**Topics**
+ [

## Frames
](#braket-frame)
+ [

## Ports
](#braket-port)
+ [

## Waveforms
](#braket-waveform)
+ [

# Working with Hello Pulse
](braket-hello-pulse.md)
+ [

# Accessing native gates using pulses
](braket-native-gate-pulse.md)

## Frames


A frame is a software abstraction that acts as both a clock within the quantum program and a phase. The clock time is incremented on each usage and a stateful carrier signal that is defined by a frequency. When transmitting signals to the qubit, a frame determines the qubit's carrier frequency, phase offset, and the time at which the waveform envelope is emitted. In Braket Pulse, constructing frames depends on the device, frequency, and phase. Depending on the device, you can either choose a predefined frame or instantiate new frames by providing a port.

```
from braket.aws import AwsDevice
from braket.pulse import Frame, Port

# Predefined frame from a device
device = AwsDevice("arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-3")
drive_frame = device.frames["Transmon_5_charge_tx"]

# Create a custom frame
readout_frame = Frame(frame_id="r0_measure", port=Port("channel_0", dt=1e-9), frequency=5e9, phase=0)
```

## Ports


A port is a software abstraction representing any input/output hardware component controlling qubits. It helps hardware vendors provide an interface with which users can interact to manipulate and observe qubits. Ports are characterized by a single string that represents the name of the connector. This string also exposes a minimum time increment that specifies how finely we can define the waveforms.

```
from braket.pulse import Port

Port0 = Port("channel_0", dt=1e-9)
```

## Waveforms


A waveform is a time-dependent envelope that we can use to emit signals on an output port or capture signals through an input port. You can specify your waveforms directly either through a list of complex numbers or by using a waveform template to generate a list from the hardware provider.

```
from braket.pulse import ArbitraryWaveform, ConstantWaveform
import numpy as np

cst_wfm = ConstantWaveform(length=1e-7, iq=0.1)
arb_wf = ArbitraryWaveform(amplitudes=np.linspace(0, 100))
```

 Braket Pulse provides a standard library of waveforms, including a constant waveform, a Gaussian waveform, and a Derivative Removal by Adiabatic Gate (DRAG) waveform. You can retrieve the waveform data through the `sample` function to draw the shape of the waveform as shown in the following example.

```
from braket.pulse import GaussianWaveform
import numpy as np
import matplotlib.pyplot as plt

zero_at_edge1 = GaussianWaveform(1e-7, 25e-9, 0.1, True)
# or zero_at_edge1 = GaussianWaveform(1e-7, 25e-9, 0.1)
zero_at_edge2 = GaussianWaveform(1e-7, 25e-9, 0.1, False)

times_1 = np.arange(0, zero_at_edge1.length, drive_frame.port.dt)
times_2 = np.arange(0, zero_at_edge2.length, drive_frame.port.dt)

plt.plot(times_1, zero_at_edge1.sample(drive_frame.port.dt))
plt.plot(times_2, zero_at_edge2.sample(drive_frame.port.dt))
```

![\[Graph showing amplitude over time for two cases: ZaE = True (lower curve) and ZaE = False (top curve). The curves have a bell shape peaking around 0.5 seconds with an amplitude of 0.10 a. u..\]](http://docs.aws.amazon.com/braket/latest/developerguide/images/gaussianwaveform.png)


The preceding image depicts the Gaussian waveforms created from `GaussianWaveform`. We chose a pulse length of 100 ns, a width of 25 ns, and an amplitude of 0.1 (arbitrary units). The waveforms are centered in the pulse window. `GaussianWaveform` accepts a boolean argument `zero_at_edges` (ZaE in the legend). When set to `True`, this argument offsets the Gaussian waveform such that the points at t=0 and t=`length` are at zero and rescales its amplitude such that the maximum value corresponds to the `amplitude` argument.

# Working with Hello Pulse


In this section, you will learn how to characterize and construct a single qubit gate directly using pulse on a Rigetti device. Applying an electromagnetic field to a qubit leads to Rabi oscillation, switching qubits between its 0 state and 1 state. With calibrated length and phase of the pulse, the Rabi oscillation can calculate a single qubit gates. Here, we will determine the optimal pulse length to measure a pi/2 pulse, an elementary block used to build more complex pulse sequences.

First, to build a pulse sequence, import the `PulseSequence` class.

```
from braket.aws import AwsDevice
from braket.circuits import FreeParameter
from braket.devices import Devices
from braket.pulse import PulseSequence, GaussianWaveform

import numpy as np
```

Next, instantiate a new Braket device using the Amazon Resource Name (ARN) of the QPU. The following code block uses Rigetti Ankaa-3.

```
device = AwsDevice(Devices.Rigetti.Ankaa3)
```

The following pulse sequence includes two components: Playing a waveform and measuring a qubit. Pulse sequence can usually be applied to frames. With some exceptions such as barrier and delay, which can be applied to qubits. Before constructing the pulse sequence you must retrieve the available frames. The drive frame is used for applying the pulse for Rabi oscillation, and the readout frame is for measuring the qubit state. This example, uses the frames of qubit 25.

```
drive_frame = device.frames["Transmon_25_charge_tx"]
readout_frame = device.frames["Transmon_25_readout_rx"]
```

Now, create the waveform that will play in the drive frame. The goal is to characterize the behavior of the qubits for different pulse lengths. You will play a waveform with different lengths each time. Instead of instantiating a new waveform each time, use the Braket-supported `FreeParameter` in pulse sequence. You are able to create the waveform and the pulse sequence once with a free parameters, and then run the same pulse sequence with different input values. 

```
waveform = GaussianWaveform(FreeParameter("length"), FreeParameter("length") * 0.25, 0.2, False)
```

Finally, put them together as a pulse sequence. In the pulse sequence, `play` plays the specified waveform on the drive frame, and the `capture_v0` measures the state from the readout frame.

```
pulse_sequence = (
    PulseSequence()
    .play(drive_frame, waveform)
    .capture_v0(readout_frame)
)
```

Scan across a range of pulse length and submit them to the QPU. Before executing the pulse sequences on a QPU, bind the value of free parameters.

```
start_length = 12e-9
end_length = 2e-7
lengths = np.arange(start_length, end_length, 12e-9)
N_shots = 100

tasks = [
    device.run(pulse_sequence(length=length), shots=N_shots)
    for length in lengths
]

probability_of_zero = [
    task.result().measurement_counts['0']/N_shots
    for task in tasks
]
```

The statistics of the qubit measurement exhibits the oscillatory dynamics of the qubit that oscillates between the 0 state and the 1 state. From the measurement data, you can extract the Rabi frequency and fine tune the length of the pulse to implement a particular 1-qubit gate. For example, from the data in figure below, the periodicity is about 154 ns. So a pi/2 rotation gate would correspond to the pulse sequence with length=38.5ns. 

![\[Line graph that shows the amount of population to the pulse duration in seconds. There are two peaks and one trough in the graph.\]](http://docs.aws.amazon.com/braket/latest/developerguide/images/Rabi-frequency.png)


## Hello Pulse using OpenPulse


 [OpenPulse](https://openqasm.com/language/openpulse.html) is a language for specifying pulse-level control of a general quantum device and is part of the OpenQASM 3.0 specification. Amazon Braket supports OpenPulse for directly programming pulses using the OpenQASM 3.0 representation.

 Braket uses OpenPulse as the underlying intermediate representation for expressing pulses in native instructions. OpenPulse supports the addition of instruction calibrations in the form of `defcal` (short for “define calibration”) declarations. With these declarations, you can specify an implementation of a gate instruction within a lower-level control grammar.

You can view the OpenPulse program of a Braket `PulseSequence` using the following command.

```
print(pulse_sequence.to_ir())
```

You can also construct an OpenPulse program directly.

```
from braket.ir.openqasm import Program
 
openpulse_script = """
OPENQASM 3.0;
cal {
    bit[1] psb;
    waveform my_waveform = gaussian(12.0ns, 3.0ns, 0.2, false);
    play(Transmon_25_charge_tx, my_waveform);
    psb[0] = capture_v0(Transmon_25_readout_rx);
}
"""
```

Create a `Program` object with your script. Then, submit the program to a QPU.

```
from braket.aws import AwsDevice
from braket.devices import Devices
from braket.ir.openqasm import Program

program = Program(source=openpulse_script)

device = AwsDevice(Devices.Rigetti.Ankaa3)
task = device.run(program, shots=100)
```

# Accessing native gates using pulses


Researchers often need to know exactly how the *native* gates supported by a particular QPU are implemented as pulses. Pulse sequences are carefully calibrated by hardware providers, but accessing them provides researchers the opportunity to design better gates or explore protocols for error mitigation such as zero noise extrapolation by stretching the pulses of specific gates.

Amazon Braket supports programmatic access to native gates from Rigetti.

```
import math
from braket.aws import AwsDevice
from braket.circuits import Circuit, GateCalibrations, QubitSet
from braket.circuits.gates import Rx

device = AwsDevice("arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-3")

calibrations = device.gate_calibrations
print(f"Downloaded {len(calibrations)} calibrations.")
```

**Note**  
Hardware providers periodically calibrate the QPU, often more than once a day. The Braket SDK enables you to obtain the latest gate calibrations.

```
device.refresh_gate_calibrations()
```

To retrieve a given native gate, such as the RX or XY gate, you need to pass the `Gate` object and the qubits of interest. For example, you can inspect the pulse implementation of the RX(π/2) applied on qubit 0.

```
rx_pi_2_q0 = (Rx(math.pi/2), QubitSet(0))

pulse_sequence_rx_pi_2_q0 = calibrations.pulse_sequences[rx_pi_2_q0]
```

You can create a filtered set of calibrations using the `filter` function. You pass a list of gates or a list of `QubitSet`. The following code creates two sets that contain all of the calibrations for RX(π/2) and for qubit 0.

```
rx_calibrations = calibrations.filter(gates=[Rx(math.pi/2)])
q0_calibrations = calibrations.filter(qubits=QubitSet([0]))
```

Now you can provide or modify the action of native gates by attaching a custom calibration set. For example, consider the following circuit.

```
bell_circuit = (
    Circuit()
    .rx(0, math.pi/2)
    .rx(1, math.pi/2)
    .iswap(0, 1)
    .rx(1, -math.pi/2)
)
```

You can run it with a custom gate calibration for the `rx` gate on `qubit 0` by passing a dictionary of `PulseSequence` objects to the `gate_definitions` keyword argument. You can construct a dictionary from the attribute `pulse_sequences` of the `GateCalibrations` object. All gates not specified are replaced with the quantum hardware provider's pulse calibration.

```
nb_shots = 50
custom_calibration = GateCalibrations({rx_pi_2_q0: pulse_sequence_rx_pi_2_q0})
task = device.run(bell_circuit, gate_definitions=custom_calibration.pulse_sequences, shots=nb_shots)
```