Quantum Control for
NV & Other Defect Centers
Discover how to leverage our advanced quantum control platform to perform groundbreaking experiments in your NV center lab with these real-life use cases.
"I regard Quantum Machines as the quintessential example of how deep-tech companies should operate in harmony. Their OPX+ devices exemplify excellence, and the team's unmatched business ethos sets them apart. From unboxing the OPX+ to the first measurements is a truly exhilarating experience. They consistently go above and beyond for their customers, ensuring mutual success. Transitioning from academia to the startup realm, I have rarely encountered a company as remarkable as Quantum Machines."
“The quantum orchestration platform (QOP) platform completely changed the way we control semiconductor quantum dot spin qubits. Key qubit control schemes we previously developed individually using time-consuming hardware description languages are now easily implemented in one box.”
“Integrating the control of the lab into a single unit made my research experience much easier. I can honestly say it is thanks to Quantum Machines’ well-engineered hardware and their team's true willingness to assist.”
“Replacing three devices with one synchronized, orchestrated machine tremendously simplified lab workflow. Now our pulse sequences run in a fraction of the time of any other device combo. Plus, we can “talk” to the FPGA in human-speak, to run real-time calculations that were too complicated before! Along with the yoga-level.”
“Dedicated hardware for controlling and operating quantum bits is something we have all been dreaming of. Quantum Machines has answered this call by allowing us and others in the field to scale up with ease and with far greater functionality than was ever possible before.”
Fig. 1. Experimental setup for dynamical decoupling using the NV center in diamond. The MW control signal is generated using IQ modulation with two analog outputs of the OPX+.
NV centers are excellent systems for sensing. Not only are they highly sensitive to various types of forces (magnetic, electric, strain, etc.), but their small size allows for spatial resolutions of a few nm. In this example, we want to demonstrate how one of the most common sensing methods with NV centers, namely dynamical decoupling (DD), can be effortlessly implemented using the Quantum Orchestration Platform (QOP). It’s a great showcase of the real-time paradigm the OPX+ operates on, which removes the need for creating long arbitrary waveforms before starting the experiment.
Dynamical decoupling is typically used in quantum control to prolong the coherence of the spin system. This is achieved by a periodic sequence of control pulses, which refocus the environmental effects and hence attenuate noise. Since phases accumulated from frequency components close to the pulse spacing are being enhanced, DD effectively acts as a frequency filter. Thus, the technique can be used for noise spectroscopy.
Fig. 1 shows the experimental setup. The OPX+ is controlling the laser via a digital marker output. Two analog outputs of the OPX+ are used for IQ modulation of the MW signal controlling the NV spin. The output pulses of the APD, which collects the fluorescence of the NV center, are sent to an analog input of the OPX+ for time tagging.
In this example, we want to focus on one of the most common DD sequences used for NV-based sensing, namely the XY8-N sequence. The XY8-N sequence consists of the following pulse sequence: \((\pi_x-\pi_y-\pi_x-\pi_y-\pi_y-\pi_x-\pi_y-\pi_x)^N\), where \(N\) is the so-called XY8 order and the indices \(x\) and \(y\) correspond to the rotation axis in the rotating frame. The pulses are spaced equidistantly with a spacing of \(\tau\). The entire sequence is shown in Fig. 2a.
The XY8 sequence is applied after the NV electron spin is brought into a superposition state \(|-1> + |0>\) by an initial \((\pi/2)_x\)-pulse. After the decoupling sequence, the spin state is mapped onto the spin population by a final \((\pi/2)_x\)-pulse (see Fig. 2a). Finally, the NV center is read out optically by a laser pulse, which simultaneously repolarizes the electron spin state. Fig. 2b shows the filter function of the sequence. The central frequency \(\nu=1/2\tau\) is defined by the periodicity of the pulses, while the width \(\Delta\nu=1/N\tau\) depends on the total acquisition time \(T=N\tau\).
Fig. 2a. XY8-N sequence with waiting time \(\tau.\)
Fig. 2b. Filter function of the XY8-N sequence. The central frequency is determined by the waiting time \(\tau,\) while the width is defined by the total acquisition time \(T=N\tau.\)
The example QUA program runs a for_each
loop, which iterates through a given list of \(\tau\) values. Additionally, an outer for
loop averages over many sweeps. In QUA, we can define macros that make code shorter and clearer. In this example, we use the macro xy8_n(n)
to create all the XY8 sequence pulses in a single line. The macro dynamically creates the XY8 sequence according to the order specified in parameter n. The macro creates all pulses and wait times by looping over another helper macro, xy8_block()
, which creates the 8 pulses of a single XY8 block. The \(\pi_y\) pulses are generated by rotating the frame of the spin using the built-in frame_rotation()
function. Then, the frame is reset back into its initial state by calling reset_frame()
.
from qm.QuantumMachinesManager import QuantumMachinesManager
from qm.qua import *
from qm import SimulationConfig
import matplotlib.pyplot as plt
import numpy as np
NV_IF = 100e6
t_min = 4
t_max = 100
dt = 1
t_vec = np.arange(t_min, t_max, dt)
repsN = 3
simulate = True
with program() as xy8:
# Realtime FPGA variables
a = declare(int) # For averages
i = declare(int) # For XY8-N
t = declare(int) # For tau
times = declare(int, size=100) # Time-Tagging
counts = declare(int) # Counts
counts_ref = declare(int)
diff = declare(int) # Diff in counts between counts & counts_ref
counts_st = declare_stream() # Streams for server processing
counts_ref_st = declare_stream()
diff_st = declare_stream()
with for_(a, 0, a < 1e6, a + 1):
play("laser", "qubit")
with for_(t, t_min, t <= t_max, t+dt): # Implicit Align
# Play meas (pi/2 pulse at x)
play("pi_half", "qubit")
xy8_n(repsN)
play("pi_half", "qubit")
measure("readout", "qubit", None, time_tagging.raw(times, 300, counts))
# Time tagging done here, in real time
# Plays ref (pi/2 pulse at -x)
play("pi_half", "qubit")
xy8_n(repsN)
frame_rotation(np.pi, "qubit")
play("pi_half", "qubit")
reset_frame('qubit') # Such that next tau would start in x.
measure("readout", "qubit", None, time_tagging.raw(times, 300, counts_ref))
# Time tagging done here, in real time
# save counts:
assign(diff, counts - counts_ref)
save(counts, counts_st)
save(counts_ref, counts_ref_st)
save(diff, diff_st)
with stream_processing():
counts_st.buffer(len(t_vec)).average().save("dd")
counts_ref_st.buffer(len(t_vec)).average().save("ddref")
diff_st.buffer(len(t_vec)).average().save("diff")
qmm = QuantumMachinesManager()
qm = qmm.open_qm(config)
job = qm.execute(xy8, duration_limit=0, time_limit=0)
def xy8_n(n):
# Assumes it starts frame at x, if not, reset_frame before
wait(t, "qubit")
xy8_block()
with for_(i, 0, i < n - 1, i + 1):
wait(2 * t, "qubit")
xy8_block()
wait(t, "qubit")
def xy8_block():
play("pi", "qubit") # 1 X
wait(2 * t, "qubit")
frame_rotation(np.pi / 2, "qubit")
play("pi", "qubit") # 2 Y
wait(2 * t, "qubit")
reset_frame("qubit")
play("pi", "qubit") # 3 X
wait(2 * t, "qubit")
frame_rotation(np.pi / 2, "qubit")
play("pi", "qubit") # 4 Y
wait(2 * t, "qubit")
play("pi", "qubit") # 5 Y
wait(2 * t, "qubit")
reset_frame("qubit")
play("pi", "qubit") # 6 X
wait(2 * t, "qubit")
frame_rotation(np.pi / 2, "qubit")
play("pi", "qubit") # 7 Y
wait(2 * t, "qubit")
reset_frame("qubit")
play("pi", "qubit") # 8 X
QUA Code for the XY8-N dynamical decoupling sequence.
For the NV centers optical readout, we utilize the built-in time tagging functionality of the QOP. A call of the measure
statement starts the time tagging. A time tag for each detected pulse from the APD is saved into the real-time array times
, and the total number of detected photons is saved into the integer variable counts
. Concurrently, the measure
statement generates a readout pulse, which here is the trigger pulse going to the laser system. The same sequence is repeated a second time with a final \((\pi/2)_{-x}\). The photons detected during this are saved in the variable counts_ref
and act as a reference signal.
At the end of each loop, we save the photon counts into a so-called stream, using the save()
function. These streams allow streaming data to the client PC while the program is still running. The stream processing feature also offers a rich library of data processing functions which can be applied to streams. The QOP server performs the processing before sending it to the client PC. It can significantly reduce the amount of transferred data by limiting it to the user’s preferred result. In this example, we use the average()
function to average the data while streaming.
A known issue with pulsed DD, is the emergence of spurious harmonics due to the pulses’ finite length. A theoretically “easy” solution is to introduce a random global phase to each iteration of the experiment [1]. This solution quickly becomes very taxing when trying to utilize it by a-priory waveform creation, because the number of repetitions we need to upload has to be very large (\(N\gg 100\)) for the phase to be considered random. As the OPX+ generates this sequence on the fly, we can utilize its internal random number generator to easily create this randomized XY8-N sequence by adding a couple lines of code to the one above:
First, we’ll define an additional variable \(\phi\).
...
phi = declare(fixed,value=0) # Random phase
...
Second, we’ll add a line assigning a new random phase for each iteration using the command Random().rand_fixed()
which returns a random number between 0 and 1.
...
assign(phi,Random().rand_fixed()*2*np.pi)
play("pi_half", "qubit")
xy8_n(repsN,phi)
...
The last step would be to add a frame_rotation()
command before each \(\pi_x\) and reset_frame()
at the end of the xy8_block macro.
...
frame_rotation(phi, "qubit")
play("pi", "qubit") # 8 X
reset_frame("qubit")
...
One of the NV’s major strengths is its ability to utilize its nuclear spin environment as a quantum register for complex protocols that involve more than a single qubit. This utility was already shown in many experiments, including quantum sensing [2,3], quantum computation [4], and quantum networks [5]. All these experiments share the need to initialize the nuclear spin to a known state, and efficiently readout that state via manipulation of the NV electron spin. In order to actively initialize the nuclear spin, we first need to be able to determine its state, so we will first look at the single-shot readout (SSRO) [6].
Here we will discuss a protocol for a SSRO on the intrinsic \(^{14}N\) nuclear spin. In general the single-shot readout consists of a conditional rotation on the NV, a laser pulse to read its state, and then repeating these two steps \(N\) times to accumulate enough photons for state separation. As the \(^{14}N\) is a spin 1, two consecutive SSRO steps are necessary. In the first SSRO, the conditional \(\pi\) pulse will be performed if the nuclear spin is at the \(|0_N>\) sublevel. This will tell us whether the nuclear spin is at the \(|0_N>\) sublevel or at the \(|\pm1_N>\) sublevels. If we are at \(|0_N>\), the readout is done, if not, then a second SSRO needs to be initiated to determine whether the nuclear spin is at the \(|+1_N>\) or \(|-1_N>\) sublevel. The ability to skip the second SSRO step exemplifies the OPX+’s capabilities, as without real-time measurement based decision making, one could waste precious nuclear spin coherence time on an unnecessary step.
As the SSRO is usually part of a larger sequence, the example QUA program is written as a macro named SSRO()
, making it easier to use for different protocols. The basic building block of the macro is composed from a conditional rotation, defined by the CnNOTe()
macro and the measure()
command.
Let’s first start with the most basic building block: the C\(_n\)NOT\(_e\) gate on the electron spin. This gate inverts the electron spin only for a specific nuclear spin state. This is achieved by choosing the correct frequency in the hyperfine resolved ODMR spectra of the NV center. In the QUA script below, we define this gate operation in function CnNOTe()
. The CnNOTe()
macro accepts the nuclear spin state for which the electron spin should be flipped as an integer (-1: \(|-1_n>\), +1: \(|+1_n>\), 0: \(|0_n>\)). It calculates the corresponding new intermediate frequency according to the given nuclear spin state by adding or subtracting the hyperfine splitting value (hf_splitting ≈ 2.16 MHz) from the central frequency \(f_0\) of the NV centers hyperfine spectra. We use the built-in QUA function update_frequency()
to update the quantum element ‘sensor’ frequency, which corresponds to the sensor spin. As a result, all of the following pulses played to this quantum element will be at this new frequency. Finally, we play a \(\pi\)-pulse to the sensor spin using the play
command, which enables us to dynamically update the pulse’s amplitude and duration, to reduce the pulse’s frequency bandwidth.
def CnNOTe(condition_state):
"""
CNOT-gate on the electron spin.
condition_state is in [-1, 0, 1] for a spin 1 nuclear or [-1, 1] for a spin half nuclear and gives the nuclear spin
state for which the electron spin is flipped.
"""
align(*all_elements)
update_frequency("sensor", NV_IF + condition_state * hf_splitting)
play("pi_x"*amp(0.1), "sensor", duration= (pi_length/4)*10)
update_frequency("sensor", NV_IF)
align(*all_elements)
QUA Code for the C\(_n\)NOT\(_e\) macro
The measure()
statement has a time tagging module, which counts the arriving photons while simultaneously executing the laser pulse. This module allows time tagging of pulses via the analog inputs of the OPX+. The arrival times of all photons detected during the detection window are saved into the real-time array time tags. Additionally, the total number of detected photons is saved into the variable counts. These two operations are written within a for loop that runs for N times to accumulate enough photons for state differentiation.
Once the for loop is over, we use the if statement to compare the total number of photons against a predefined threshold, SSRO_threshold. If the number of photons is lower than the threshold, the nuclear spin is at the \(|0_N>\), and we can continue with the experiment. If the number of photons is higher than the threshold, we need to determine whether the nuclear spin is in the
\(|+1_N>\) or \(|-1_N>\) states. This is done by repeating the SSRO, this time with the CnNOTe()
receiving the integer 1. After the loop, we know the nuclear spin is at the \(|+1_N>\) if the photon count is lower than the threshold, and \(|-1_N>\) if it is higher.
def SSRO(N, result):
"""Determine the state of the nuclear spin"""
i = declare(int)
res_vec = declare(int, size=10)
counts = declare(int)
ssro_count = declare(int,value=0)
# run N repetitions
with for_(i, 0, i < N, i + 1):
wait(100, "sensor")
CnNOTe(0)
measure(
"readout",
"sensor",
None,
time_tagging.analog(res_vec, 300, counts),
)
assign(ssro_count, ssro_count + counts) # sums up the total photons detected during the SSRO
# compare photon count to threshold and save result in variable "state" or continue to the next step
with if_(ssro_count < SSRO_threshold):
assign(result, 0)
with else_():
assign(ssro_count, 0)
with for_(i, 0, i < N, i + 1):
wait(100, "sensor")
CnNOTe(1)
measure(
"readout",
"sensor",
None,
time_tagging.analog(res_vec, 300, counts),
)
assign(ssro_count, ssro_count + counts) # sums up the total photons detected during the SSRO
# compare photon count to threshold and save result in variable "state"
with if_(ssro_count < SSRO_threshold):
assign(result, 1)
with else_():
assign(result, -1)
QUA Code for the single shot readout macro in the case of \(^{14}N\) nuclear spin.
The OPX+ real-time capabilities become even more prominent when one wants to initialize the nuclear spin to a specific state (\(|0_N>\) for example). Instead of postselection, which wastes time performing unwanted experiments or employing an elaborate Swap gate (as it is a 3 level system), which takes a long time and usually has limited fidelity, the OPX+ enables the user to precisely perform the necessary operation based on the nuclear spin SSRO result.In the case of nuclear spin initialization, we would start, like above, with a SSRO on the \(|0_N>\) sublevel. If the nuclear spin is at that level, we are done. If not, a second SSRO is performed to determine whether the nuclear spin is at the \(|+1_N>\) or \(|-1_N>\). Based on the result of the second SSRO, an RF \(\pi\) pulse on resonance with the desired transition can then be applied on the nuclear spin to bring it back to the \(|0_N>\). If a \(\pi\) pulse was performed, we can repeat the process to make sure the nuclear spin is in the desired state.The code example is written in the init_nuclear_spin()
, which starts with the SSRO()
macro to initially determine the nuclear spin state. Then, if it is not in the \(|0_N>\), the code enters an indeterministic while loop, until the SSRO()
determines the nuclear spin is in the \(|0_N>\) state. After each SSRO, a selective \(\phi\) pulse will be applied to the nuclear spin with a frequency determined by the result of the SSRO.
def init_nuclear_spin():
state = declare(int)
SSRO(N_SSRO, state)
with while_(state == ~0):
with if_(state == -1):
# assigning a frequency on resonanace with the 0 -> -1 nuclear transition
update_frequency("memory", memory_IF_m1)
play("pi_x", "memory")
update_frequency("memory", memory_IF_p1)
with else_():
play("pi_x", "memory")
SSRO(N_SSRO, state)
QUA Code for active initialization of the \(^{14}N\) nuclear spin.
Fig. 3. Setup for nanoscale NMR using a nuclear spin memory. The MW control sequence for the electron spin is created using IQ modulation with two analog outputs of the OPX+ (blue). The RF signal for nuclear spin manipulation is directly synthesized with the OPX+ (orange). The pulses of the APD are time-tagged by the OPX+ via one of the analog inputs (red).
With the QOP and QUA, we can write even the most complex experiments as short and clear single programs. To demonstrate this, let’s look at an NV-based NMR experiment that utilizes a nuclear spin as an additional memory [2,3]. It is possible to drastically enhance the spectral resolution by using the long lifetime of the nuclear spin as a resource. This technique allows nanoscale NMR with chemical contrast, e.g. [7].
NMR using NV centers is typically based on imprinting the Larmor precession of sample spins into the phase of a superposition state of the NV electron spin state [8]. We can achieve this, for example, by using Ramsey spectroscopy, Hahn echo sequences or dynamical decoupling. The spectral resolution of these methods is limited by the duration of the phase accumulation period, and consequently, is limited by the coherence time \(T_2^{sens}\) of the sensor spin. It’s possible to overcome this limitation by performing correlation spectroscopy [9] . Here, the signal is generated by correlating the results of two subsequent phase accumulation sequences separated by the correlation time \(T_C\). During the correlation time, the phase information is stored (partially) in the polarization of the sensor spin. Hence, the possible correlation time, and therefore the spectral resolution, is limited by the spin-relaxation time \(T_1^{sens}\) (\(>T_2^{sens}\)) of the sensor.
It’s possible to improve this even further by utilizing a memory spin, which has a much longer longitudinal lifetime. In the correlation spectroscopy experiment we discuss here, the information is stored on the nuclear spin (memory) instead of on the NV spin (sensor). As a result, the achievable correlation time is significantly increased. The intrinsic nitrogen nuclear spin of the NV center is a perfect candidate to act as this memory spin. It is strongly coupled to the NV center electron spin, which acts as the sensor, while its coupling to other electron or nuclear spin is negligible. When applying a strong bias magnetic field (3T) aligned along the NV-axis, we can achieve memory lifetimes \(T_1^{mem}\) on the order of several seconds. In this example, we assume that the used NV center incorporates a 14N nuclei with a 1-spin and the eigenstates \(|+1_n>\), \(|-1_n>\) and \(|0_n>\).
Fig. 4. A sequence of NMR with memory spin. It consists of active initialization of the memory spin, encoding, sample manipulation, decoding, and a final readout of the memory spin via single-shot readout.
Fig 4 shows the complete sequence. It consists of five steps: initialization, encoding, sample manipulation, decoding, and readout. The encoding aims to encode the sample sensor interaction into the spin population of the memory spin. First, the memory spin is brought from its initial state \(|0_n>\) into a superposition state by a \(\pi/2\)-pulse. Next, entanglement between sensor and memory is established for two phase-accumulation windows. The entanglement is created and destroyed by nuclear spin state selective \(\pi\)-pulses performed on the sensor spin. While the sensor and memory spins are entangled, the interaction with the sample spins leads to a phase accumulation on the memory spin superposition state. In between the two phase-accumulation periods, the sample is actively flipped by a resonant \(\pi\)-pulse. The final phase \(\Delta\phi=\phi_2 – \phi_1\), where \(\phi_1\) and \(\phi_2\) are the accumulated phases during the first and second accumulation window respectively, is then mapped into the memory spin population by a final \(\pi/2\)-pulse on the memory spin. The decoding sequence is identical, except for the conditions of the C\(_n\)NOT\(_e\) gates on the sensor spin.
For initialization and readout, we use the macros SSRO()
and init_nuclear_spin()
, that are described above.
from qm.qua import *
from qm.QuantumMachinesManager import QuantumMachinesManager
from configuration import *
import numpy as np
all_elements = ["sensor", "sample", "memory"]
N_avg = 1e6
N_SSRO = 5000
hf_splitting = 2.16e6 # N14 hyperfine splitting (NV_IF is moved by -1.08e6)
t_e = 2000
tau_vec = [int(i) for i in np.arange(1e3, 5e4, 5e3)]
SSRO_threshold = 200
with program() as prog:
"""
Main script
"""
n = declare(int)
tau = declare(int)
result_vec = declare(int, size=len(tau_vec))
c = declare(int)
with for_(n, 0, n < N_avg, n + 1):
assign(c, 0)
with for_each_(tau, tau_vec):
init_nuclear_spin()
encode(t_e)
align(*all_elements)
play("pi_2", "sample")
wait(tau, "sample")
play("pi_2", "sample")
align(*all_elements)
play("laser", "sensor")
decode(t_e)
SSRO(N_SSRO, result_vec[c])
assign(c, c + 1)
with for_(n, 0, n < result_vec.length(), n + 1):
save(result_vec[n], "result")
def init_nuclear_spin():
state = declare(int)
SSRO(N_SSRO, state)
with while_(state == ~0):
with if_(state == -1):
# assigning a frequency on resonanace with the 0 -> -1 nuclear transition
update_frequency("memory", memory_IF_m1)
play("pi_x", "memory")
update_frequency("memory", memory_IF_p1)
with else_():
play("pi_x", "memory")
SSRO(N_SSRO, state)
def SSRO(N, result):
"""Determine the state of the nuclear spin"""
i = declare(int)
res_vec = declare(int, size=10)
counts = declare(int)
ssro_count = declare(int,value=0)
# run N repetitions
with for_(i, 0, i < N, i + 1):
wait(100, "sensor")
CnNOTe(0)
measure(
"readout",
"sensor",
None,
time_tagging.analog(res_vec, 300, counts),
)
assign(ssro_count, ssro_count + counts) # sums up the total photons detected during the SSRO
# compare photon count to threshold and save result in variable "state" or continue to the next step
with if_(ssro_count < SSRO_threshold):
assign(result, 0)
with else_():
assign(ssro_count, 0)
with for_(i, 0, i < N, i + 1):
wait(100, "sensor")
CnNOTe(1)
measure(
"readout",
"sensor",
None,
time_tagging.analog(res_vec, 300, counts),
)
assign(ssro_count, ssro_count + counts) # sums up the total photons detected during the SSRO
# compare photon count to threshold and save result in variable "state"
with if_(ssro_count < SSRO_threshold):
assign(result, 1)
with else_():
assign(result, -1)
def CnNOTe(condition_state):
"""
CNOT-gate on the electron spin.
condition_state is in [-1, 0, 1] for a spin 1 nuclear or [-1, 1] for a spin half nuclear and gives the nuclear spin
state for which the electron spin is flipped.
"""
align(*all_elements)
update_frequency("sensor", NV_IF + condition_state * hf_splitting)
play("pi_x"*amp(0.1), "sensor", duration= (pi_length/4)*10)
update_frequency("sensor", NV_IF)
align(*all_elements)
def encode(t):
"""
Play the encoding sequence with wait time t.
"""
align(*all_elements)
reset_frame("memory")
play("pi_2_x", "memory")
CnNOTe(0)
wait(t // 4, "sensor")
CnNOTe(0)
play("pi", "sample")
CnNOTe(1)
wait(t // 4, "sensor")
CnNOTe(0)
play("pi_2_y", "memory")
align(*all_elements)
def decode(t):
"""
Play the decoding sequence with wait time t.
"""
align(*all_elements)
play("pi_2_x", "memory")
CnNOTe(1)
wait(t // 4, "sensor")
CnNOTe(0)
play("pi", "sample")
CnNOTe(1)
wait(t // 4, "sensor")
CnNOTe(1)
play("pi_2_y", "memory")
align(*all_elements)
qmm = QuantumMachinesManager()
qm = qmm.open_qm(config)
job = qm.execute(prog)
QUA Code for NMR with a nuclear spin memory as detailed in figure 4.
The encoding (decoding) sequence is defined in the function encode()
(decode()
). The different pulses are executed using play
statements and the CnNOTe()
macro. The QUA-function align()
is used to define the timing of the individual pulses. One of the basic principles of QUA is that every command is executed as early as possible. Hence, when not specified otherwise, pulses played on different quantum elements are played in parallel. To ensure that pulses play in a specific order, we use the built-in align()
function. This function causes all specified quantum elements to wait until all previous commands are complete, and so it aligns them in time.
Finally, the main script runs the whole sequence for different values of the waiting time of the Ramsey sequence played on the sample spin in a foreach
loop. Additionally, the experiment is averaged over N_avg
repetitions by an outer for
loop. The result for each value of is saved into the corresponding item of the result vector result_vec
. Then, the result vector is saved element-wise using the save()
function and streamed to the user PC.
References:[1] Z. Wang et al., “Randomisation of Pulse Phases for Unambiguous and Robust Quantum Sensing”, Phys. Rev. Lett. 122, 200403 (2019)[2] S. Zaiser et al., “Enhancing quantum sensing sensitivity by a quantum memory”, Nature Comm. 7, 12279 (2016)
[3] M. Pfender et al., “Nonvolatile nuclear spin memory enables sensor-unlimited nanoscale spectroscopy of small spin clusters”, Nature Comm. 8, 834 (2017)
[4] T. H. Taminiau et al., “Universal control and error correction in multi-qubit spin registers in diamond”, Nature nano. 9, 171–176 (2014)
[5] M. Pompili et al., “Realization of a multi-node quantum network of remote solid-state qubits” , Science 372, 6539 (2021)
[6] P. Neumann et al., “Single-Shot Readout of a Single Nuclear Spin”, Science 329, 542-544 (2010)
[7] N. Aslam et al., “Nanoscale nuclear magnetic resonance with chemical resolution”, Science 357, 67-71 (2017)
[8] T. Staudacher et al., “Nuclear Magnetic Resonance Spectroscopy on a (5-Nanometer)^3 Sample Volume”, Science 339, 561-563 (2013)
[9] A. Laraoui et al., “High-resolution correlation spectroscopy of 13C spins near a nitrogen-vacancy centre in diamond”, Nature Comm. 4, 1651 (2013)
Have a specific experiment in mind and wondering about the best quantum control and electronics setup?
Want to see what our quantum control and cryogenic electronics solutions can do for your qubits?