How to encode phase in Presto#

There are several ways to encode the phase of pulses in Presto. As an example, we will look at a few different ways of implementing ±X and ±Y single-qubit gates, and discuss in what scenarios you might choose one over the others.

The ±X gate is a π rotation around the x axis of the Bloch sphere. And similarly, the ±Y gate is a π rotation around the y axis. What determines the axis of rotation in the Bloch sphere is the phase of the qubit-control drive pulses. We can encode this phase either in the IQ (real and imaginary) amplitudes or in the phase of the intermediate-frequency (IF) generator.

1. No IF generators#

If you can avoid using an intermediate frequency and set the numerically-controlled oscillator (NCO, the carrier in the digital mixer) directly at the target frequency, then this is probably the easiest method using Presto. When you use external, analog IQ mixers and local oscillators you never want to use zero IF to avoid 1/f noise and to avoid mixer artifacts like LO leakage. With digital up-conversion this problems do not exist, so it is in general a good idea to use zero IF.

In the following example we encode the phase in the complex amplitude of the pulses. The pulse has a square envelope in this example, but can have any arbitrary shape.

NCO_FREQ = 5.2 * 1e9  # Hz
pls.hardware.configure_mixer(freq=NCO_FREQ, out_ports=OUT_PORT, in_ports=IN_PORT)

# pulse shape
shape = np.ones(2_000, dtype=np.complex128)

amp_px = +1   # +X
amp_py = +1j  # +Y
amp_mx = -1   # -X
amp_my = -1j  # -Y

plus_x = pls.setup_template(output_port=OUT_PORT, group=0, template=amp_px * shape)
plus_y = pls.setup_template(output_port=OUT_PORT, group=0, template=amp_py * shape)
minus_x = pls.setup_template(output_port=OUT_PORT, group=0, template=amp_mx * shape)
minus_y = pls.setup_template(output_port=OUT_PORT, group=0, template=amp_my * shape)

Note how we do not set envelope=True in the templates: we are not using the IF generators at all (the ones we would configure with setup_freq_lut()), and so pulses will be output at NCO_FREQ directly.

2. Encode the IF frequency and phase into templates#

If you want/need to use a nonzero IF, then one option is to encode it directly in the templates as a complex wave instead of using the IF generators.

NCO_FREQ = 5.1 * 1e9
IF_FREQ = 100 * 1e6

pls.hardware.configure_mixer(freq=NCO_FREQ, out_ports=OUT_PORT, in_ports=IN_PORT)

# pulse shape
t_arr = np.arange(2_000) / pls.get_fs("dac")
shape = np.ones_like(t_arr, dtype=np.complex128)
omega_t = 2 * np.pi * IF_FREQ * t_arr
template_px = shape * np.exp(1j * (omega_t + 0 * np.pi / 2))  # +X
template_py = shape * np.exp(1j * (omega_t + 1 * np.pi / 2))  # +Y
template_mx = shape * np.exp(1j * (omega_t + 2 * np.pi / 2))  # -X
template_my = shape * np.exp(1j * (omega_t + 3 * np.pi / 2))  # -Y

plus_x = pls.setup_template(output_port=OUT_PORT, group=0, template=template_px)
plus_y = pls.setup_template(output_port=OUT_PORT, group=0, template=template_py)
minus_x = pls.setup_template(output_port=OUT_PORT, group=0, template=template_mx)
minus_y = pls.setup_template(output_port=OUT_PORT, group=0, template=template_my)

Note how we still don’t use the IF generators, and we encode the IF directly into the raw data points of the templates. If IF_FREQ is positive you get upper sideband, if it’s negative you get lower sideband. The pulses will be output at NCO_FREQ + IF_FREQ.

3. Use phases of IF generators#

We can use the IF frequency generator and upload 4 different phases into the look-up table (LUT). Then, during a pulse sequence we can step through the values of the LUT to output different pulses. To output a high sideband (at frequency NCO_FREQ + IF_FREQ) use phases_q=phases - np.pi / 2 and to output the low sideband (at frequency NCO_FREQ - IF_FREQ) use phases_q=phases + np.pi / 2.

phases = np.array([0.0, np.pi / 2, np.pi, 3 * np.pi / 2])
	frequencies=IF_FREQ * np.ones_like(phases),
	phases_q=phases - np.pi / 2,

shape = np.ones(2_000, dtype=np.complex128)  # <-- square pulse, put your shape here
pulse = pls.setup_template(
	output_port=OUT_PORT, group=0, template=(1 + 1j) * shape, envelope=True

# define experimental sequence
T = 0.0
for phase_index in [0, 1, 2, 3]:  # [+X +Y -X -Y]
	pls.select_frequency(T, index=phase_index, output_ports=OUT_PORT, group=0)
	pls.output_pulse(T, pulse)
	T += 100e-6

In setup_template() we pass envelope=True to indicate this template should be multiplied with the IF generator on group 0. Note that it is important to set the same shape for both real and imaginary parts of the template in order to get single-sideband mixing.

4. Use two IF generators#

It is sometimes useful to think about signals in terms of quadratures (I and Q) instead of thinking about them in terms of amplitude and phase. We can use two IF generators to encode one quadrature each. By applying two templates simultaneously (each multiplied by one IF generator) and adjusting their amplitudes we can output a pulse of arbitrary amplitude and phase. This is useful when sweeping a 2D grid in an IQ plane.

pls.hardware.configure_mixer(freq=NCO_FREQ, out_ports=OUT_PORT, in_ports=IN_PORT)

pls.setup_freq_lut(OUT_PORT, 0, IF_FREQ, phase, phase-np.pi/2)
pls.setup_freq_lut(OUT_PORT, 1, IF_FREQ, phase+np.pi/2, (phase+np.pi/2)-np.pi/2)

plus_x  = pls.setup_long_drive(OUT_PORT, 0, 2e-6, amplitude=  1+1j,  envelope=True)
plus_y  = pls.setup_long_drive(OUT_PORT, 1, 2e-6, amplitude=  1+1j,  envelope=True)
minus_x = pls.setup_long_drive(OUT_PORT, 0, 2e-6, amplitude=-(1+1j), envelope=True)
minus_y = pls.setup_long_drive(OUT_PORT, 1, 2e-6, amplitude=-(1+1j), envelope=True)

If we want to output a pulse of arbitrary phase phi and amplitude A, we can encode this information in the scale look-up tables of two groups and output pulses plus_x and plus_y simultaneously:

complex_scale = A * np.exp(1j * phi)
pls.setup_scale_lut(OUT_PORT, 0, complex_scale.real)
pls.setup_scale_lut(OUT_PORT, 1, complex_scale.imag)

T = 0
pls.output_pulse(T, [plus_x, plus_y])

Notice that the phase phi and the amplitude A can be vectors, thus allowing us to change both the amplitude and the phase of the output pulse by stepping only through the amplitude look-up tables when we define our pulse sequence.

It is possible to use either setup_long_drive() or setup_template() in all four cases. setup_long_drive is used if we need to sweep the length of the pulse, and setup_template is used if we want to output an arbitrary shape of the pulse.

Spectrum of the pulses#

No matter which way you choose to output the pulses with different phases, the pulses themselves are identical in all four cases. This is because the signal generation is entirely digital. What approach you choose might depend on your preference, or sometimes, when creating complex sequences, one of the four ways might be the only way.

In all four cases the spectrum of the pulses looks like in the picture below. The 2 μs square pulse at 5.2 GHz is as expected. There is no LO leakage (at 5.1 GHz) and the lower sideband (5 GHz) is not present. What is present as a consequence of digital up-conversion is an image of our signal at 2.8 GHz (chosen up-conversion mode DacMode.Mixed42) and a sharp peak at 8 GHz (chosen sampling frequency DacFSample.G8). These far away spurious signals are easily filtered by a band pass filter (Mini-Circuits VBFZ-5500-S+ is used in the figure below).

{image} images/if_in_template_light.svg :align: center :class: only-light