Use Case 1: Individual Addressing of Nuclear Spins
As the field progresses and complexity increases, the control system requirements become more demanding, and the need for real-time feedback arises. As a concrete example, we consider the experiment performed by Abobeih et al. [2]. In this work, the authors mapped the location of 27 individually addressed \({}^{13}C\) atoms in a diamond lattice, using an NMR-like technique, with a single nearby NV center as the quantum sensor. A marvel of quantum photonics.
In experiments of such complexity, achieving individual addressing of each nuclear spin is a very demanding challenge. The authors used a variation of a dynamical decoupling (DD) sequence, named DDRF [3], that involves the introduction of an RF pulse, in resonance with the individual nuclear spin, interleaved in between DD pulses applied to the electron spin. Such an arduous sequence was undoubtedly an electronics and programming feat.
Let’s Run this with the OPX+
The author’s approach has several nuanced requirements. Running the sequence with the OPX+ instead, allows highlighting some of its unique capabilities. For example, to have constructive phase accumulation on the nuclear spin, it is vital to maintain precise control on the RF pulse relative phase. The OPX+ generates its pulses with automatic hardware-level tracking of the phase accumulation of each output frequency, taking care of the problem of relative phase without the need for user intervention. By default, all pulses are performed in the rotating frame of the qubit. The phase is preserved when transitioning back and forth between frequencies on the same output channel.
Furthermore, the control over the phase allows for defining both single-qubit rotations and two-qubit conditional gates on nuclear spin. The addition of a global phase allows for control over the gate’s rotation axis. With an OPX+, this is as simple as writing the command \(frame\_rotation (\phi)\) with real-time parametric waveform generation. Essentially, this means that \(\phi\) can be a variable living on the processor. Thus it is possible to adaptively update it during the sequence, based on live inputs from the measurement.
Fig. 1 shows the simulated DDRF sequence for unconditional (Fig. 1a-b) and conditional (Fig. 1c-d) rotations. We can achieve the latter by adding a phase \(\pi\) for every other pulse. As mentioned by the authors, the DDRF sequence allows for parallel control of several nuclear spins, as the nuclear spin frequency does not restrict the interpulse delay of the electron DD. Even though the authors do not explicitly demonstrate it in their work, this important addition is readily done with the OPX+, which offers easy-to-use multiplexing capabilities. Fig. 2 shows an example of multiplexing of two frequencies, as output of two different channels (Fig. 2a-b) or summed and sent through the same output channel (Fig. 2c-d).
The possibilities do not stop at just two frequencies either. The OPX+ has 18 pulser cores capable of working in parallel, allowing the control of up to 16 nuclear spins simultaneously (using 2 for the electron spin). The OPX+ allows such control to be performed in parallel and with unmatched ease, thanks to our python-based programming language QUA.
Fig. 1 a-b) Unconditional and c-d) conditional rotations on the nuclear spin, using the DDRF sequence. b) and d) shows a zoomed-in view of a) and b), respectively, to highlight the pulse synchronization. The blue (yellow) represents the output for the I (Q) channels for the electron spin in an NV center qubit. As no intermediate frequency is defined, these can be considered the envelope of the \(\pi_x\) and \(\pi_y\) pulses, respectively. The green represents the output for the nuclear spin RF, where the frequency was chosen arbitrarily.
Fig. 2 DDRF sequence for unconditional rotation of nuclear spins, with two RF frequencies as output a-b) in parallel to two different channels (green and red) and c-d) multiplexed to the same output channel. b) and d) shows a zoomed-in view of a) and b), respectively, to highlight the pulse synchronization. The blue (yellow) represents the output for the I (Q) channels for the electron spin in an NV center qubit.
Controlling Nuclear Spin with QUA
The ability to make macros that simplify the code is one of the advantages of QUA. It allows you to troubleshoot and re-use compartmentalized code that makes writing new sequences exceptionally fast. As an example, let’s write the above DDRF sequence.
First, we define the macro, xy8_block()
, which creates the 8 MW pulses, to the electron spin, of a single XY8 block. The play("pi", "spin_qubit")
tells the OPX+ to output a pulse named pi
, both predefined in a setup specific configuration file. The wait()
command is used for the interpulse delay, while we use frame_rotation()
and reset_frame()
to control the phase of the pulses. Notice that as the pulser tracks the phase of the rotating frame, the phase defined in the frame_rotation()
command is simply the phase of the Y pulse in the rotating frame, i.e., \(\pi /2\).
def xy8_block():
# A single XY8 block, ends at x frame.
play("pi", "spin_qubit") # 1 X
wait(tt, "spin_qubit")
frame_rotation(np.pi / 2, "spin_qubit")
play("pi", "spin_qubit") # 2 Y
wait(tt, "spin_qubit")
reset_frame("spin_qubit")
play("pi", "spin_qubit") # 3 X
wait(tt, "spin_qubit")
frame_rotation(np.pi / 2, "spin_qubit")
play("pi", "spin_qubit") # 4 Y
wait(tt, "spin_qubit")
play("pi", "spin_qubit") # 5 Y
wait(tt, "spin_qubit")
reset_frame("spin_qubit")
play("pi", "spin_qubit") # 6 X
wait(tt, "spin_qubit")
frame_rotation(np.pi / 2, "spin_qubit")
play("pi", "spin_qubit") # 7 Y
wait(tt, "spin_qubit")
reset_frame("spin_qubit")
play("pi", "spin_qubit") # 8 X
The second macro to complete the electron spin part of the XY8-N sequence is the xy8_n()
macro, allowing us to create the full sequence:
def xy8_n(n):
i = declare(int)
wait(t, "spin_qubit")
xy8_block()
with for_(i, 0, i < n - 1, i + 1):
wait(tt, "spin_qubit") # tt = 2*t
xy8_block()
wait(t, "spin_qubit")
The for()
loop is written in QUA, and this means the processor doesn’t get a code that is replicated n times, but a command to repeat in real-time the code written within the loop. Thus, there is no limit on how long the XY8 can be memory-wise, and long sequences won’t cause a long overhead time in waveform uploads. QUA allows all control flow operations to run on the pulse processor. Similar to the XY8 macros, we can then define the operations necessary for the RF pulses that control the nuclear spin, first a single RF block or rf_block()
, then rf_n()
.
Using these macros the DDRF sequence is straightforward. For example, if we want to apply an unconditional rotation on nuclear_spin1
,
play("pi2", "spin_qubit")
xy8_n(N)
rf_n(N, 'nuclear_spin1', False)
play("pi2", "spin_qubit")
Then, adding a second conditional rotation on nuclear_spin2
is a single line of code.
play("pi2", "spin_qubit")
xy8_n(N)
rf_n(N, 'nuclear_spin1', False)
rf_n(N, 'nuclear_spin2', True)
play("pi2", "spin_qubit")
The macros are executed on different threads, and so they will run in parallel, saving time and reducing complexity. Fig. 3 shows the result of such a pulse sequence.
Fig. 3 a) DDRF sequence with two RF frequencies multiplexed on the same output channel (green), simultaneously applying an unconditional rotation on the first nuclear spin and a conditional rotation on the second nuclear spin. b) shows a zoomed-in view of a) to highlight the pulse synchronization. The blue (yellow) represents the output for the I (Q) channels for the electron spin.
It’s easy to embed such a sequence within a QUA loop, updating the frequency of the RF pulse dynamically. If we then also include a measurement command with the capability of time tagging photons received by a photo-diode, then we obtain a full DDRF sequence within a spectroscopy scan, running dynamically on the pulse processor:
with for_(freq, f_min, freq <= f_max, freq + df): # Implicit Align
update_frequency("nuclear_spin1", freq)
play("pi2", "spin_qubit")
xy8_n(repsN)
rf_n(repsN, 'nuclear_spin1', False)
play("pi2", "spin_qubit")
measure("readout", "green_laser", None, time_tagging.analog(times, 300, counts_ref))