# 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. ```python 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 {meth}`~.Pulsed.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. ```python 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`. ```python phases = np.array([0.0, np.pi / 2, np.pi, 3 * np.pi / 2]) pls.setup_freq_lut( output_ports=OUT_PORT, group=0, frequencies=IF_FREQ * np.ones_like(phases), phases=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) pls.store(T) T += 100e-6 ``` In {meth}`~.Pulsed.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. ```python 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: ```python 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 {meth}`~.Pulsed.setup_long_drive` or {meth}`~.Pulsed.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 ``` ```{image} images/if_in_template_dark.svg :align: center :class: only-dark ```