Optically Detected Magnetic Resonance

Optically Detected Magnetic Resonance (ODMR) is a spectroscopic technique used to study electron spins in materials. It involves applying sequences of optical and microwave pulses to manipulate and probe the spin states of electrons. By analyzing the response of the material’s photoluminescence to these pulses, valuable information about the spin properties and dynamics can be obtained. This technique is particularly useful for studying spin-based quantum technologies and materials such as diamond NV centers.

This tutorial shows how to set up a pulsed ODMR experiment using our Time Taggers and Pulse Streamer. Pulsed ODMR offers distinct advantages over continuous mode, including enhanced contrast and reduced sensitivity to laser power fluctuations. Leveraging Time Taggers provides precise timing control for accurate data acquisition and analysis, making it ideal for probing fast spin dynamics with high resolution.

../_images/ODMR_setup.svg

Creation of optical and microwave pulse patterns

In this tutorial we consider a spin-1 system. In most sequences, an initial optical pulse serves to initialize the system, e.g., preparing it in state |m_s = 0\rangle. Following this initialization, a series of microwave (MW) pulses and free evolution periods manipulate the spin state. Finally, a second optical pulse interrogates the spin state projection along the (|m_s = 0\rangle, |m_s = +1\rangle) basis, often via optical emission or absorption intensity, while also resetting the system for subsequent measurements. Various pulse sequences are commonly employed for different measurements:

1. Rabi Oscillation Sequence: In this sequence, a single MW pulse of variable duration is applied, causing the spin to oscillate between the |m_s = 0\rangle and |m_s = +1\rangle states. The amplitude of the Rabi oscillations provides information about the energy splitting between the two states. From this measurement the duration of \pi- and \pi/2-pulses can be inferred as half and quarter periods of the oscillations, respectively.

2. Ramsey Sequence: This sequence involves two \pi/2 MW pulses separated by a variable delay. The delay time determines the phase relationship between the two pulses, affecting the interference pattern observed in the spin state. Analysis of the Ramsey fringes allows for precise measurement of the spin coherence time T_2^*.

3. Hahn Echo Sequence: In this sequence, a \pi/2-pulse is followed by a delay time and then a \pi-pulse. The second pulse flips the spin state, and the delay allows for dephasing to occur. The echo signal observed after the \pi/2-pulse provides information about the spin dephasing time T_2.

../_images/ODMR-1.svg

To create any of these sequences, we make use of the Sequence class of our Pulse Streamer 8/2. This class allows to independently set pulse patterns on each channel. A pulse pattern is represented by a tuple in the form (duration, level). The duration is always specified in nanoseconds and the level is either 0 or 1 for digital output.

First of all, we connect to the Pulse Streamer.

# Change the following line to use your specific Pulse Streamer IP address
ip_hostname='169.254.8.2'
pulser = PulseStreamer(ip_hostname)

Next, we declare the channels names, and all the relevant parameters for creating a sequence to measure Rabi Oscillations and quantifying the duration of a \pi-pulse. To achieve this, we need to create the following patterns:

  1. A pattern to drive the laser, enabling it to emit optical pulses for system initialization and readout.

  2. A pattern to drive a high isolation microwave switch for gating the microwave pulses.

  3. A pattern for synchronization purposes.

  4. A pattern to measure the incoming photons during the readout pulse for time-gated analysis.

# Channel names
optical_ch   = 0
mw_ch   = 1
sync_ch = 2
gate_ch = 3
# Digital levels
HIGH=1
LOW=0

# Change the values according to your experiment
period = 8000 # period of each pattern
init_pulse_dur = 2000 # width of initialization optical pulses
readout_pulse_dur = 300 # width of readout optical pulses
pause = 1000 # time between a readout and next initializazion pulse

We define then a set of values over which the MW pulse duration (\tau) is swept, to ultimately build the desired pulse patterns.

tau = np.arange(100, 2000, 100)

# For each selected MW pulse width, the experiment is repeated multiple times
n_loops = 1000

# Optical pattern
optical_patt = [(init_pulse_dur, HIGH), (period-init_pulse_dur-readout_pulse_dur-pause, LOW),
                (readout_pulse_dur, HIGH), (pause, LOW)]*n_loops*len(tau)

# MW pattern
# Initialize the MW pulse pattern
mw_patt = []
for ti in tau:
    mw_patt.extend([(init_pulse_dur, LOW), (ti, HIGH), (period-init_pulse_dur-ti, LOW)]*n_loops)

# Pulse pattern that marks a new value of a MW pulse duration
sync_patt = [(10, HIGH), (period*n_loops-10, LOW)]*len(tau)
# Add last sync pulse to the pattern to mark the end of the acquisition
sync_patt.extend([(10, HIGH)])

#  Pattern for gating detector clicks
gate_patt = [(period-readout_pulse_dur-pause, LOW), (readout_pulse_dur,HIGH),
             (pause, LOW)]*n_loops*len(tau)

# Set channels using class Sequence
seq = Sequence()
seq.setDigital(optical_ch, optical_patt)
seq.setDigital(mw_ch, mw_patt)
seq.setDigital(sync_ch, sync_patt)
seq.setDigital(gate_ch, gate_patt)


# Display your sequence
seq.plot()

Signal generation and detection

To perform ODMR measurements, we connect the Time Tagger and declare the channels used. Here, we can also adjust the hardware settings for each channel.

tagger = createTimeTagger()
gate = 1
sync = 2
detector = 3

Then, we need to filter the incoming detector clicks revealed by the Time Tagger according to the generated gate pattern. This can be done at the software level by employing the virtual channel GatedChannel. The rising and falling edges of the gate signal are used to open and close each time gate, respectively.

gated_detector_vch = GatedChannel(tagger, detector, gate, -gate)
# Get the channel number that will be used in the CBM measurement
gated_detector = gated_detector_vch.getChannel()

Next, we set up a CountBetweenMarkers measurement to count the filtered events on a detector channel between sync events, that herald the change of the MW pulse width. The counts are accumulated in an array whose number of bins is equal to the number of \tau values.

cbm = CountBetweenMarkers(tagger, gated_detector, sync, CHANNEL_UNUSED, len(tau))
cbm.start()
tagger.sync()

Now that the measurement is set up, the sequence can be run by the Pulse Streamer and the events counted by the Time Tagger. We run the sequence once

n_runs = 1
final = OutputState.ZERO()
pulser.stream(seq, n_runs, final)

and we collect the data

ready = False

# Periodically get data until the CountBetweenMarkers completes the acquisition
while ready is False:
    time.sleep(.2)
    # Check if the measurement is ready
    ready = cbm.ready()
    counts = cbm.getData()
    # You may add progress visualization code here

Sweeping modes

There are different ways to acquire and accumulate data in these pulsed ODMR experiments. We report here two protocols to collect events to visualize Ramsey oscillations.

In the first method, the interpulse delay \tau is swept and the data are collected in a single acquisition step after executing the whole sequence. The example code is analogous to the one of the previous paragraph, except for the construction of the Ramsey sequence.

dur_pi = 200 # Change value according to the outcome of Rabi measurement
tau = np.arange(100, 1500, 100) # Interpulse time delay

mw_patt = []
for ti in tau:
    mw_patt.extend([(init_pulse_dur, LOW), (dur_pi/2, HIGH), (ti, LOW),
                    (dur_pi/2, HIGH), (period-ti-dur_pi-init_pulse_dur, LOW)]*n_loops)

In the second approach, we repeat the sequence for the same interpulse delay multiple times, as usual, and accumulate the data before changing the interpulse delay.

#The optical, the sync and gate patterns do not depend on the interpulse delay
optical_patt = [(init_pulse_dur, HIGH), (period-init_pulse_dur-readout_pulse_dur-pause, LOW),
                (readout_pulse_dur, HIGH), (pause, LOW)]*n_loops
sync_patt = [(10, HIGH), (period*n_loops-10, LOW)]
sync_patt.extend([(10, HIGH)])
gate_patt = [(period-readout_pulse_dur-pause, LOW), (readout_pulse_dur,HIGH), (pause, LOW)]*n_loops


seq = Sequence()
seq.setDigital(optical_ch, optical_patt)
seq.setDigital(sync_ch, sync_patt)
seq.setDigital(gate_ch, gate_patt)

counts = []

# For each interpulse delay, events on the detector channel
# between two sync marker events are accumulated
cbm = CountBetweenMarkers(tagger, gated_detector, sync, CHANNEL_UNUSED, 1)
tagger.sync()

mw_patt = []
for ti in tau:
    mw_patt = [ (init_pulse_dur, LOW), (dur_pi/2, HIGH), (ti, LOW),
                    (dur_pi/2, HIGH), (period-ti-dur_pi-init_pulse_dur, LOW)]*n_loops

    seq.setDigital(mw_ch, mw_patt)

    cbm.start()

    # Run the sequence
    pulser.stream(seq, n_runs, final)

    ready = False

    # Get data every while
    while ready is False:
        time.sleep(.2)
        ready = cbm.ready()
        data = cbm.getData()

    # Store the total counts for each interpulse delay into an array
    counts.append(data)

ODMR contrast

Quantifying the ODMR contrast, defined as the differential photoluminescence signal between measurements with and without applying microwave radiation, is crucial for several reasons. It serves as a direct indicator of the efficiency of spin manipulation and the sensitivity of the measurement setup. High contrast values typically correspond to better signal-to-noise ratios, enabling more precise determination of resonance frequencies and spin relaxation times.

The schematics of this measurement is reported in the figure below. This trivial sequence is repated for each different microwave frequency selected, to obtain in the end an ODMR spectrum.

../_images/ODMR-2.svg

For the data acquisitions, there are two possible implementations:

1. A single CountBetweenMarkers measurement: the rising and falling edges of the gate are used as begin_channel and end_channel, respectively, and 2N as n_values, where N is the number of frequencies to measure. In the output array, one gets, for each frequency, the counts while the MW is off in the even bins (0,2,4,6,…) and the counts while the MW is on in the odd bins (1,3,5,7,…).

cbm = CountBetweenMarkers(tagger, detector, gate, -gate, 2N)

2. Two different CountBetweenMarkers measurements: one collects counts when the MW frequency is on and one when the MW frequency is off. In this case MW indicator pulses are needed as markers. For the first CountBetweenMarkers the rising and the falling edges of the MW indicator pulse are used as begin_channel and end_channel, respectively. On the contrary, for the second measurement the falling edge of the MW indicator pulse is used as begin_channel and the rising edge as end_channel. The gate signal should be used to filter the detector clicks at the software level using the the virtual channel GatedChannel, as described in the previous paragraph. This ensures the same acquisition time, for each frequency, when the microwave of and when it is off.

mw_ind = 4

# Create a SynchronizedMeasurements instance that allows you to process the same tags
synchronized = SynchronizedMeasurements(tagger)
sync_tagger_proxy = synchronized.getTagger()

# Accumulate counts when the MW is on
cbm_mw_on = CountBetweenMarkers(sync_tagger_proxy, gated_detector, mw_ind, -mw_ind, N)

# Accumulate counts when the MW is off
cbm_mw_off = CountBetweenMarkers(sync_tagger_proxy, gated_detector, -mw_ind, mw_ind, N)