See also

A Jupyter notebook version of this tutorial can be downloaded here.

image0

Single NV Center Full Tuneup Notebook#

The control solution for Color Centers#


In this application example, we expand on the Pulsed ODMR application example, using our Qblox Scheduler software package for color center characterization.

Color centers are unique defects in solid state where the quantum state can be read out via an optically active transition. Well known examples are the nitrogen-vacancy (NV) centers and tin-vacancy (SnV) centers in diamond or silicon carbide. The coupling of the quantum state to a photon make color centers extremely suitable for quantum networks where quantum computing nodes are entangled over large distances.

QBLOX Cluster#

Qblox hardware integrates all control signals, photon counting and time tagging in a single quantum controller. The integrated framework takes care of synchronization of the experiment and low-latency conditional feedback operations; setting up a complete experiment has never been so seamless.

The figure below illustrates the role of individual modules of the Qblox’ portfolio in such color center experiments.

image1

  • Qubit Control Module (QCM) can be used to control the acousto-optic modulator (AOM) for the laser beam.

  • Qubit Control Module - RF (QCM-RF II) controls the qubit state, driving th 0-1 transition.

  • Qubit Timetag Module (QTM) connects to photodetectors for counting and time-tagging fluorescent photons. ***

Experimental flow#

The first step in bringing up a quantum chip is to characterize a single qubit. The details of the characterization depend on the specific type of color center but it always involves a set of experiments run sequentially. Each step is upstreamed by the results of the previous one. The workflow for tuning a color center is illustrated below:

image1

In this diagram, arrows indicate dependencies, showing that experiments are typically conducted from left to right.

Qubit spectroscopy in color centers is performed through Optically Detected Magnetic Resonance (ODMR);

  • Sweeping the microwave drive frequency using the QCM-RF II reveals the \(|0\rangle\)-\(|1\rangle\) qubit transition frequency through the intensity of the scattered fluorescence light.

  • Spin-photon conversion for photonic readout is realized by coupling the \(\left| 0 \right\rangle\) state to the \({}^3E\) excited manifold, also involved in a radiative transition cycle.

  • Counting/timetagging of the fluorescence photons is handled by the QTM connected to a photodector.

Specifically for Nitrogen-Vacancy (NV) centers, the optical transition does not require perfect resonance of the the laser. The system is excited into the excited state with a spin preserving transition (often referred to as “green” laser). The spin dependent decay paths result in a bright and dark response:

  • The \(|0\rangle\) state is the bright state, producing, in typical single NV centers in bulk diamond or nanodiamonds, 20 to 100 kcounts per second under standard confocal microscopy.

  • The \(|1\rangle\) state is the dark state, resulting in a reduced fluorescence rate, with the contrast typically being 20% to 30%.

Imports#

Runtime components imports#

[1]:
from __future__ import annotations  # noqa: I001
from typing import Any

from qblox_scheduler import HardwareAgent, Schedule, ClockResource, BasicElectronicNVElement

from qblox_scheduler.enums import BinMode

from qblox_scheduler.operations.expressions import DType
from qblox_scheduler.operations.loop_domains import linspace, arange
from qblox_scheduler.operations import (
    Measure,
    SetClockFrequency,
    SquarePulse,
    VoltageOffset,
    TriggerCount,
    IdlePulse,
    MarkerPulse,
    LoopStrategy,
)

import json
/.venv/lib/python3.14/site-packages/quantify_core/utilities/general.py:13: QCoDeSDeprecationWarning: The `qcodes.utils.helpers` module is deprecated. Please consult the api documentation at https://microsoft.github.io/Qcodes/api/index.html for alternatives.
  from qcodes.utils.helpers import NumpyJSONEncoder

Initializing the HardwareAgent#

Here we instantiate the HardwareAgent class using hardware (i.e. Qblox Cluster) and quantum device (i.e. qubits) parameters - see start-up guide for NV centers and Qblox Scheduler Hello World for further details.

In short, the Hardware Configuration file contains the information specific to Qblox hardware, while the Device Configuration file (QuantumDevice) contains device elements (BasicElectronicNVElement), where we save the various qubit parameters. Here we are loading a template for 1 qubit.

[2]:
## Set to True if not using real cluster
using_dummy: bool = True

## Set to True if using QTM for counting photons, if using QRM set to False
using_qtm: bool = True

# Device Config
device_path = "dependencies/configs/nv_center1q.yaml"

# Hardware config — single base file, patched per setup
with open("dependencies/configs/nv_center_hw.json") as f:
    hw_config: dict[str, Any] = json.load(f)

match (using_dummy, using_qtm):
    case (True, _):
        pass  # Base config is ready for dummy use

    case (False, True):
        hw_config["hardware_description"]["cluster0"]["ip"] = "10.10.200.42"
        del hw_config["hardware_description"]["cluster0"]["modules"]
        # Adjust QTM input to match physical wiring
        hw_config["connectivity"]["graph"] = [
            ["cluster0.module10.digital_input_5", "qe0:optical_readout"]
            if edge[1] == "qe0:optical_readout"
            else edge
            for edge in hw_config["connectivity"]["graph"]
        ]

    case (False, False):
        hw_config["hardware_description"]["cluster0"]["ip"] = "10.10.200.42"
        del hw_config["hardware_description"]["cluster0"]["modules"]
        # QRM handles optical readout instead of QTM
        hw_config["connectivity"]["graph"] = [
            ["cluster0.module4.real_input_0", "qe0:optical_readout"]
            if edge[1] == "qe0:optical_readout"
            else edge
            for edge in hw_config["connectivity"]["graph"]
        ]
        hw_config["hardware_options"]["sequencer_options"] = {
            "qe0:optical_readout-qe0.ge0": {"ttl_acq_threshold": 0.5}
        }
        hw_config["hardware_options"]["digitization_thresholds"] = {
            "qtm0:in-digital": {"analog_threshold": 0.8},
            "qtm1:in-digital": {"analog_threshold": 0.8},
        }
[3]:
# Create a new hardware agent based on the configuration files discussed above
hw_agent: HardwareAgent = HardwareAgent(
    hardware_configuration=hw_config, quantum_device_configuration=device_path
)

hw_agent.connect_clusters()

# Create an alias for a specific element of the DUT to conveniently interact with its properties
qubit: BasicElectronicNVElement = hw_agent.quantum_device.get_element("qe0")
/.venv/lib/python3.14/site-packages/qblox_scheduler/qblox/hardware_agent.py:499: UserWarning: cluster0: Trying to instantiate cluster with ip 'None'.Creating a dummy cluster.
  warnings.warn(

Continuous Wave ODMR#

A Continuous Wave (CW) Optically Detected Magnetic Resonance (ODMR) experiment is performed to determine roughly the \(|0\rangle\)-\(|1\rangle\) qubit transition frequency. With that technique, the resolution is limited by the linewidth of the laser in use. With typical setups, it is not possible to distinguish the hyperfine splitting. The CW ODMR schedule continuously stimulates the optical transition for readout, while at the same time sweeping the microwave drive frequency. The continuous stimulation of the optical transition results in higher photon counts, making it a good experiment to start with and verify the proper tuning of the confocal optical setup. It also eliminates dependencies on the timing of the laser with respect to the acquisition window (the laser is not pulsed) and – in case of direct laser control – the laser stability.

The CW ODMR schedule consists for following steps:

  1. Laser activation

    • The laser beam is turned on continuously for the entire duration of the experiment. This is done by setting a VoltageOffset to the optical control port with clock ge0.

    • This serves the purpose of both qubit ground state initialization as well as readout.

  2. Microwave Frequency Sweep

    • The microwave signal frequency is set using SetClockFrequency at the qubit’s microwave port.

    • A SquarePulse applies the microwave tone, sweeping through frequencies to probe the \(|0\rangle\)-\(|1\rangle\) transition.

  3. Readout

    • Since the laser beam is always turned on, we only need to accumulate the photon counts via a TriggerCount operation, which is done at the same time as when the microwave pulse is played.

    • We accumulate photon counts over many repetitions, grouping results as per the specified acquisition coordinates (frequency), across multiple repetitions.

Parametrization of the schedule#

Generate a range of frequencies with the defined steps, for the CW ODMR spectroscopy.

[4]:
# Number of repetitions for the whole schedule
repetitions: int = 2

# Frequency settings
frequency_center: float = qubit.clock_freqs.spec  # Hz
frequency_width: float = 20e6  # Hz
frequency_npoints: int = 4

## In this schedule, we want to mix some pulse-level and gate-level operations.
## For this, we fetch the qubit parameters from the central data structure containing all the qubit
## information, the Device Config file, initialized as `qubit` above.

# Define control parameters for the laser beam used for readout
clock_laser: str = f"{qubit.name}.ge0"
port_laser: str = qubit.ports.optical_control
amp_laser: float = qubit.measure.pulse_amplitude
duration_laser: float = qubit.measure.pulse_duration

# Acquisition parameters
clock_meas: str = f"{qubit.name}.ge0"
port_meas: str = qubit.ports.optical_readout
duration_meas: float = qubit.measure.acq_duration

#  Microwave drive pulse parameters
clock_drive: str = f"{qubit.name}.spec"
port_drive: str = f"{qubit.name}:mw"
amp_drive: float = qubit.rxy.amp180
duration_drive: float = (
    duration_meas  # We keep the microwave pulse on for as long as we count the photonss
)
[5]:
# creation of the LinearDomain objects used in the Schedule loops
reps_loop = arange(repetitions, dtype=DType.NUMBER)
freqs_loop = linspace(
    start=frequency_center - frequency_width / 2,
    stop=frequency_center + frequency_width / 2,
    num=frequency_npoints,
    dtype=DType.FREQUENCY,
)
[6]:
# Initialize the schedule with the specified number of repetitions
sched: Schedule = Schedule("CW ODMR")

## Turn on the laser beam for the entire experiment duration.
sched.add(
    VoltageOffset(
        offset_path_I=amp_laser,
        offset_path_Q=amp_laser,
        port=port_laser,
        clock=clock_laser,
    )
)

## Creating a sweep for repetitions and to scan frequency
with (
    sched.loop(reps_loop),
    sched.loop(freqs_loop) as freq,
):
    ## Microwave drive pulse

    ### Sweeping through the 0-1 transition frequencies
    sched.add(SetClockFrequency(clock=clock_drive, clock_freq_new=freq))

    ## Choose Square or Hermite Pulse for driving
    drive_pulse = sched.add(
        SquarePulse(amp=amp_drive, duration=duration_meas, port=port_drive, clock=clock_drive)
    )

    # Trigger scope
    sched.add(
        MarkerPulse(duration=duration_meas - 4e-9, port="qe0:marker"),
        ref_op=drive_pulse,
        ref_pt="start",
        rel_time=0,
    )

    # Counting the photons
    sched.add(
        TriggerCount(
            port=port_meas,
            clock=clock_meas,
            duration=duration_meas,
            acq_channel="counts",
            coords={"freq": freq},
            bin_mode=BinMode.AVERAGE_APPEND,
        ),
        rel_time=0,
        ref_op=drive_pulse,
        ref_pt="start",
    )

# Turn off the reaodut laser
sched.add(VoltageOffset(offset_path_I=0.0, offset_path_Q=0.0, port=port_laser, clock=clock_laser))

# `VoltageOffset` requires a Q1ASM upd_param call which has minimum duration 4 ns
sched.add(IdlePulse(duration=4e-9))

# Execute the experiment
cw_odmr_dataset = hw_agent.run(sched)
/tmp/ipykernel_9284/3906357044.py:22: FutureWarning: clock_freq_new is deprecated as an argument to SetClockFrequency and will be removed in qblox-scheduler >= 2.0; use frequency instead.
  sched.add(SetClockFrequency(clock=clock_drive, clock_freq_new=freq))
/tmp/ipykernel_9284/3906357044.py:26: FutureWarning: amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.
  SquarePulse(amp=amp_drive, duration=duration_meas, port=port_drive, clock=clock_drive)

Let us observe the dataset

[7]:
cw_odmr_dataset
[7]:
<xarray.Dataset> Size: 128B
Dimensions:                 (acq_index_counts: 4)
Coordinates:
  * acq_index_counts        (acq_index_counts) int64 32B 0 1 2 3
    loop_repetition_counts  (acq_index_counts) float64 32B 0.0 1.0 2.0 3.0
    freq                    (acq_index_counts) float64 32B 2.87e+09 ... 2.89e+09
Data variables:
    counts                  (acq_index_counts) float64 32B -1.0 -1.0 -1.0 -1.0
Attributes:
    tuid:     20260415-121223-715-39d1af
[8]:
# Compiling the schedule to see the compiled instructions and pulse diagram

comp_sched = hw_agent.compile(sched)

Let us view the compiled Q1ASM instructions

[9]:
# Uncomment to display the compiled Q1ASM instructions and QCoDeS setting for each module

# hw_agent.latest_compiled_schedule.compiled_instructions

Looking at the Pulse Diagram

[10]:
# Uncomment to see the pulse diagram
comp_sched.plot_pulse_diagram(plot_backend="plotly")
[11]:
# Update the transition frequency
# This can be done directly in the device config file (dependencies/configs/nv_center1q) or in the following manner
# qubit.clock_freqs.spec = ## INSERT UPDATED VALUE

Pulsed ODMR Schedule#

Pulsed Optically Detected Magnetic Resonance (ODMR) probes the \(|0\rangle\)-\(|1\rangle\) qubit transition by sweeping a microwave frequency using the QCM-RF II, which drives the transition when it matches the resonance frequency. The readout process involves activating the laser with the QCM to excite the color center, causing it to fluoresce, and detecting emitted photons with the QRM/QTM.

The Pulsed ODMR schedule consists for following steps:

  1. Ground State Initialization

    • The laser beam is turned on for a fixed duration by sending a SquarePulse to the optical control port with clock ge0.

    • This ensures the qubit state is initialized to \(|0\rangle\) regardless of prior conditions.

  2. Microwave Frequency Sweep

    • The microwave frequency is set using SetClockFrequency at the qubit’s microwave port.

    • A SquarePulse/SkewedHermitePulse applies the microwave tone, sweeping through frequencies to probe the \(|0\rangle\)-\(|1\rangle\) transition.

  3. Readout

    • The Measure gate, or (for pulse level) a SquarePulse followed by TriggerCount operation, can be used. This will:

      • Activate the laser beam (clock ge0) for a set duration, followed by an acquisition with a time-of-flight delay.

      • Accumulate the photon counts, grouping results as per the specified acquisition coordinates (frequency), across multiple repetitions.

Generate a range of frequencies with the defined steps, for the Pulsed ODMR spectroscopy.

[12]:
# Adjust the parameters for sweeping
repetitions = 2
# Frequency settings
frequency_center = qubit.clock_freqs.spec  # Hz
frequency_width = 10e6  # Hz
frequency_npoints = 4

# Define control parameters for laser beam
amp_laser = qubit.measure.pulse_amplitude
duration_laser = qubit.measure.pulse_duration

#  Drive pulse parameters
amp_drive = qubit.rxy.amp180
duration_drive = qubit.rxy.duration
[13]:
# Initialize the schedule with the specified number of repetitions
sched = Schedule("Pulsed ODMR")


## Add an initial optical reset pulse
sched.add(SquarePulse(amp=amp_laser, duration=duration_laser, port=port_laser, clock=clock_laser))

## Creating a sweep for repetitions and to scan frequency
with (
    sched.loop(linspace(0, repetitions, repetitions, DType.NUMBER)),
    sched.loop(
        linspace(
            start=frequency_center - frequency_width / 2,
            stop=frequency_center + frequency_width / 2,
            num=frequency_npoints,
            dtype=DType.FREQUENCY,
        )
    ) as freq,
):
    ## Microwave drive pulse

    ### Sweeping through the 0-1 transition frequencies
    sched.add(SetClockFrequency(clock=clock_drive, clock_freq_new=freq))
    ## Choose Square or Hermite Pulse for driving
    drive_pulse = sched.add(
        SquarePulse(amp=amp_drive, duration=duration_drive, port=port_drive, clock=clock_drive)
    )
    # drive_pulse = sched.add(SkewedHermitePulse(duration=duration_drive, amplitude=amp_drive, skewness=0, phase=0, port=port_drive, clock=clock_drive))

    # Trigger Scope
    sched.add(
        MarkerPulse(duration=duration_drive, port="qe0:marker"),
        ref_op=drive_pulse,
        ref_pt="start",
        rel_time=0,
    )

    ### Gate level - using all the parameters from measure in device config, and clock ge0
    sched.add(
        Measure(
            qubit.name,
            coords={"frequency": freq},
            acq_protocol="TriggerCount",
            acq_channel="counts",
            bin_mode=BinMode.AVERAGE_APPEND,
        )
    )

    ## Optical relaxation time
    sched.add(IdlePulse(duration=500e-9))


# Execute the experiment
odmr_dataset = hw_agent.run(sched)
/tmp/ipykernel_9284/2046428951.py:6: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

/tmp/ipykernel_9284/2046428951.py:23: FutureWarning:

clock_freq_new is deprecated as an argument to SetClockFrequency and will be removed in qblox-scheduler >= 2.0; use frequency instead.

/tmp/ipykernel_9284/2046428951.py:26: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

Let us observe the dataset

[14]:
odmr_dataset
[14]:
<xarray.Dataset> Size: 128B
Dimensions:                 (acq_index_counts: 4)
Coordinates:
  * acq_index_counts        (acq_index_counts) int64 32B 0 1 2 3
    loop_repetition_counts  (acq_index_counts) float64 32B 0.0 1.0 2.0 3.0
    frequency               (acq_index_counts) float64 32B 2.875e+09 ... 2.88...
Data variables:
    counts                  (acq_index_counts) float64 32B -1.0 -1.0 -1.0 -1.0
Attributes:
    tuid:     20260415-121224-680-e1856d
[15]:
# Update the transition frequency
# This can be done directly in the device config file (dependencies/configs/nv_center1q) or in the following manner
# qubit.clock_freqs.spec = ## INSERT UPDATED VALUE
[16]:
# Compiling the schedule to see the compiled instructions and pulse diagram

comp_sched = hw_agent.compile(sched)

Let us view the compiled Q1ASM instructions

[17]:
# Uncomment to display the compiled Q1ASM instructions and QCoDeS setting for each module

# hw_agent.latest_compiled_schedule.compiled_instructions

Looking at the Pulse Diagram

[18]:
# Uncomment to see the pulse diagram
comp_sched.plot_pulse_diagram(plot_backend="plotly")

Pulsed ODMR versus Power Schedule#

Power calibration of the microwave pulse is crucial to optimal performance. At sufficiently low power one can resolve the hyperfine structure of the qubit energy levels, and exclude excessive power broadening. To resolve the hyperfine structure of the qubit energy levels, pulsed ODMR is often performed across varying power levels. Additionally, after identifying the qubit’s transition frequency, optimizing the microwave power is crucial for better precision.

In Qblox Scheduler, it’s easy to combine frequency and power sweeps within a single schedule. It only requires to add a few minor modifications to the existing Pulsed ODMR schedule.

In the newly defined schedule, the amplitudes to sweep over are provided via the spec_amps parameter. In the loop, for each frequency (spec_freq), the amplitude of the spectroscopy operation is adjusted, enabling efficient frequency and power optimization in one seamless schedule. It’s that simple!

[19]:
# We can adjust the width and resolution of the frequency sweep, now that we know the transition frequency.
frequency_center = qubit.clock_freqs.spec
frequency_width = 5e6
frequency_npoints = 4

repetitions = 2

# Initialize the schedule
sched = Schedule("Pulsed ODMR versus Power")

# Add an initial optical reset pulse
sched.add(SquarePulse(amp=amp_laser, duration=duration_laser, port=port_laser, clock=clock_laser))

with (
    sched.loop(linspace(0, repetitions, repetitions, DType.NUMBER)),
    sched.loop(
        linspace(
            start=frequency_center - frequency_width / 2,
            stop=frequency_center + frequency_width / 2,
            num=frequency_npoints,
            dtype=DType.FREQUENCY,
        )
    ) as spec_freq,
    # Adjust the amplitudes to be swept through
    sched.loop(linspace(start=0.3, stop=1, num=5, dtype=DType.AMPLITUDE)) as spec_amp,
):
    ## Microwave drive pulse
    sched.add(SetClockFrequency(clock=clock_drive, clock_freq_new=spec_freq))
    ## Choose Square or Hermite Pulse for drive
    drive_pulse = sched.add(
        SquarePulse(amp=spec_amp, duration=duration_drive, port=port_drive, clock=clock_drive)
    )
    # drive_pulse = sched.add(SkewedHermitePulse(duration=duration_drive, amplitude=spec_amp, skewness=0, phase=0, port=port_drive, clock=clock_drive))

    ## Marker to trigger scope
    sched.add(
        MarkerPulse(duration=duration_drive, port="qe0:marker"),
        ref_op=drive_pulse,
        ref_pt="start",
        rel_time=0,
    )

    ## Measure
    sched.add(
        Measure(
            qubit.name,
            coords={"freq": spec_freq, "amp": spec_amp},
            acq_protocol="TriggerCount",
            bin_mode=BinMode.AVERAGE_APPEND,
        )
    )

    ## Optical relaxation time
    sched.add(IdlePulse(duration=500e-9))
/tmp/ipykernel_9284/1758519934.py:12: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

/tmp/ipykernel_9284/1758519934.py:28: FutureWarning:

clock_freq_new is deprecated as an argument to SetClockFrequency and will be removed in qblox-scheduler >= 2.0; use frequency instead.

/tmp/ipykernel_9284/1758519934.py:31: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

[20]:
# # Execute the experiment

power_odmr_dataset = hw_agent.run(sched)
[21]:
# Once again seeing the dataset
power_odmr_dataset
[21]:
<xarray.Dataset> Size: 800B
Dimensions:            (acq_index_0: 20)
Coordinates:
  * acq_index_0        (acq_index_0) int64 160B 0 1 2 3 4 5 ... 15 16 17 18 19
    amp                (acq_index_0) float64 160B 0.3 0.475 0.65 ... 0.825 1.0
    loop_repetition_0  (acq_index_0) float64 160B 0.0 1.0 2.0 ... 17.0 18.0 19.0
    freq               (acq_index_0) float64 160B 2.878e+09 ... 2.882e+09
Data variables:
    0                  (acq_index_0) float64 160B -1.0 -1.0 -1.0 ... -1.0 -1.0
Attributes:
    tuid:     20260415-121225-333-3e3a98
[22]:
# Update the transition frequency
# qubit.clock_freqs.spec = ## INSERT UPDATED VALUE
[23]:
# Compiling the schedule to see the compiled instructions and pulse diagram

comp_sched = hw_agent.compile(sched)

Let us view the compiled Q1ASM instructions

[24]:
# Uncomment to display the compiled Q1ASM instructions and QCoDeS setting for each module

# hw_agent.latest_compiled_schedule.compiled_instructions

Looking at the Pulse Diagram

[25]:
# Uncomment to see the pulse diagram
comp_sched.plot_pulse_diagram(plot_backend="plotly")

Multiplexed Pulsed ODMR Schedule#

Now that we can observe the hyperfine structure, we can enhance the contrast of the signal by driving the system in a multipexed fashion. Instead of a single drive tone that can only drive a single transition at a time, we will frequency multiplex three tones to simultaneously excite multiple transitions depending on the overlap. In this way, five dips will be present in the signal, with the middle one showing three times as much contrast (as all three transitions will be excited at once). The multiplexing can be recognized in the schedule as three clocks are introduced; each representing a separate tone.

[26]:
repetitions = 2
frequency_npoints = 4
frequency_center = qubit.clock_freqs.spec
frequency_width = 10e6

# Define the difference of the hyperfine split tones from the main microwave tone
del_1 = -3e6
del_2 = 4e6
clock_del1 = f"{qubit.name}.del1"
clock_del2 = f"{qubit.name}.del2"

# Initialize the schedule
sched = Schedule("Multiplexed Pulsed ODMR")

# Let us add the 2 extra clocks. Note that they must also be specified in the HW config in the "modulation_frequencies" section of "hardware_options"
sched.add_resource(ClockResource(name=clock_del1, freq=frequency_center + del_1))
sched.add_resource(ClockResource(name=clock_del2, freq=frequency_center + del_2))

## Add an initial optical reset pulse
sched.add(SquarePulse(amp=amp_laser, duration=duration_laser, port=port_laser, clock=clock_laser))

## Creating a sweep for repetitions and to scan frequency
with (
    sched.loop(linspace(0, repetitions, repetitions, DType.NUMBER)),
    sched.loop(
        linspace(
            start=frequency_center + del_1 - frequency_width / 2,
            stop=frequency_center + del_1 + frequency_width / 2,
            num=frequency_npoints,
            dtype=DType.FREQUENCY,
        ),
        linspace(
            start=frequency_center - frequency_width / 2,
            stop=frequency_center + frequency_width / 2,
            num=frequency_npoints,
            dtype=DType.FREQUENCY,
        ),
        linspace(
            start=frequency_center + del_2 - frequency_width / 2,
            stop=frequency_center + del_2 + frequency_width / 2,
            num=frequency_npoints,
            dtype=DType.FREQUENCY,
        ),
    ) as (del_freq1, freq, del_freq2),
    # Adjust the amplitudes to be swept through
    # Note that since 3 tones are being multiplexed, the maximum amplitude per tone is 0.33
    sched.loop(linspace(start=0.1, stop=0.33, num=5, dtype=DType.AMPLITUDE)) as spec_amp,
):
    ## Microwave drive pulse
    ### Sweeping through the 0-1 transition frequencies
    sched.add(SetClockFrequency(clock=clock_drive, clock_freq_new=freq))
    sched.add(SetClockFrequency(clock=clock_del1, clock_freq_new=del_freq1))
    sched.add(SetClockFrequency(clock=clock_del2, clock_freq_new=del_freq2))

    # Adding the 3 tones we want to play
    drive_pulse = sched.add(
        SquarePulse(amp=spec_amp, duration=duration_drive, port=port_drive, clock=clock_drive)
    )

    sched.add(
        SquarePulse(amp=spec_amp, duration=duration_drive, port=port_drive, clock=clock_del1),
        ref_op=drive_pulse,
        ref_pt="start",
        rel_time=0,
    )

    sched.add(
        SquarePulse(amp=spec_amp, duration=duration_drive, port=port_drive, clock=clock_del2),
        ref_op=drive_pulse,
        ref_pt="start",
        rel_time=0,
    )

    # Marker Pulse for scope triggering
    sched.add(
        MarkerPulse(duration=duration_drive, port="qe0:marker"),
        ref_op=drive_pulse,
        ref_pt="start",
        rel_time=0,
    )

    ### Gate level - using all the parameters from measure in device config, and clock ge0
    sched.add(
        Measure(
            qubit.name,
            coords={"frequency": freq, "amp": spec_amp},
            acq_protocol="TriggerCount",
            acq_channel="counts",
            bin_mode=BinMode.AVERAGE_APPEND,
        )
    )

    ## Optical relaxation time
    sched.add(IdlePulse(duration=500e-9))
/tmp/ipykernel_9284/255317368.py:20: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

/tmp/ipykernel_9284/255317368.py:51: FutureWarning:

clock_freq_new is deprecated as an argument to SetClockFrequency and will be removed in qblox-scheduler >= 2.0; use frequency instead.

/tmp/ipykernel_9284/255317368.py:52: FutureWarning:

clock_freq_new is deprecated as an argument to SetClockFrequency and will be removed in qblox-scheduler >= 2.0; use frequency instead.

/tmp/ipykernel_9284/255317368.py:53: FutureWarning:

clock_freq_new is deprecated as an argument to SetClockFrequency and will be removed in qblox-scheduler >= 2.0; use frequency instead.

/tmp/ipykernel_9284/255317368.py:57: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

/tmp/ipykernel_9284/255317368.py:61: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

/tmp/ipykernel_9284/255317368.py:68: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

[27]:
# # Execute the experiment

multiplexed_odmr_dataset = hw_agent.run(sched)
[28]:
# Once again seeing the dataset
multiplexed_odmr_dataset
[28]:
<xarray.Dataset> Size: 800B
Dimensions:                 (acq_index_counts: 20)
Coordinates:
  * acq_index_counts        (acq_index_counts) int64 160B 0 1 2 3 ... 17 18 19
    loop_repetition_counts  (acq_index_counts) float64 160B 0.0 1.0 ... 19.0
    amp                     (acq_index_counts) float64 160B 0.1 0.1575 ... 0.33
    frequency               (acq_index_counts) float64 160B 2.875e+09 ... 2.8...
Data variables:
    counts                  (acq_index_counts) float64 160B -1.0 -1.0 ... -1.0
Attributes:
    tuid:     20260415-121226-918-a55ce8
[29]:
# Update the transition frequency
# qubit.clock_freqs.spec = ## INSERT UPDATED VALUE
[30]:
# Compiling the schedule to see the compiled instructions and pulse diagram

comp_sched = hw_agent.compile(sched)

Let us view the compiled Q1ASM instructions

[31]:
# Uncomment to display the compiled Q1ASM instructions and QCoDeS setting for each module

# hw_agent.latest_compiled_schedule.compiled_instructions

Looking at the Pulse Diagram

[32]:
# Uncomment to see the pulse diagram
comp_sched.plot_pulse_diagram(plot_backend="plotly")

Time Domain Measurements#

From this point on we will be using set frequencies, obtained from the preceding experiments, and trying to optimize the performance on manipulating the quantum state. For optimal performance, the mixer in the QCM-RF module should be calibrated. Enabling automated mixer calibration can be done in the hardware config (["hardware_options"]["mixer_corrections"][port-clock combination]["auto_lo_cal"/"auto_sideband_cal"]), as shown in the hardware config file for this experiment. For more details, please see the Qblox Scheduler User Guide - Hardware Config page. For the most optimal performance, the mixer can also be calibrated manually. More information on this can be found in the Manual mixer calibration tutorial.


Rabi Oscillations - Power and Time#

Now that we have calibrated the \(|0\rangle\)-\(|1\rangle\) transition frequency, we will try to coherently control the quantum state. We will vary both the power and the length of the microwave excitation pulse varied; a power-time Rabi.

[33]:
# Initialize the schedule with the specified number of repetitions
sched = Schedule("Power & Time Rabi")

repetitions = 2

duration_start = 100e-9
duration_end = 1000e-9
duration_npoints = 11

# Add an initial optical reset pulse
sched.add(SquarePulse(amp=amp_laser, duration=duration_laser, port=port_laser, clock=clock_laser))

with (
    sched.loop(linspace(0, repetitions, repetitions, DType.NUMBER)),
    sched.loop(linspace(start=0.3, stop=1, num=5, dtype=DType.AMPLITUDE)) as spec_amp,
    sched.loop(
        linspace(start=duration_start, stop=duration_end, num=duration_npoints, dtype=DType.TIME),
        strategy=LoopStrategy.UNROLLED,
    ) as spec_duration,
):
    ## Microwave drive pulse
    ## Choose Square or Hermite Pulse for drive
    drive_pulse = sched.add(
        SquarePulse(amp=spec_amp, duration=spec_duration, port=port_drive, clock=clock_drive)
    )
    # drive_pulse = sched.add(SkewedHermitePulse(duration=spec_duration, amplitude=spec_amp, skewness=0, phase=0, port=port_drive, clock=clock_drive))
    sched.add(
        MarkerPulse(duration=spec_duration, port="qe0:marker"),
        ref_op=drive_pulse,
        ref_pt="start",
        rel_time=0,
    )

    ## Measure
    sched.add(
        Measure(
            qubit.name,
            coords={"amp": spec_amp, "duration": spec_duration},
            acq_protocol="TriggerCount",
            bin_mode=BinMode.AVERAGE_APPEND,
        ),
        ref_op=drive_pulse,
        rel_time=0,
        ref_pt="end",
    )

    ## Optical relaxation time
    sched.add(IdlePulse(duration=500e-9))
/tmp/ipykernel_9284/2721225411.py:11: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

/tmp/ipykernel_9284/2721225411.py:24: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

[34]:
# # Execute the experiment

rabi_dataset = hw_agent.run(sched)
[35]:
# Once again seeing the dataset
rabi_dataset
[35]:
<xarray.Dataset> Size: 2kB
Dimensions:            (acq_index_0: 55)
Coordinates:
  * acq_index_0        (acq_index_0) int64 440B 0 1 2 3 4 5 ... 50 51 52 53 54
    duration           (acq_index_0) float64 440B 1e-07 1e-07 ... 1e-06 1e-06
    amp                (acq_index_0) float64 440B 0.3 0.475 0.65 ... 0.825 1.0
    loop_repetition_0  (acq_index_0) float64 440B 0.0 1.0 2.0 ... 2.0 3.0 4.0
Data variables:
    0                  (acq_index_0) float64 440B -1.0 -1.0 -1.0 ... -1.0 -1.0
Attributes:
    tuid:     20260415-121229-082-136b18
[36]:
# Set the relevant parameters
# qubit.rxy.amp180 = ## INSERT VALUE HERE
# qubit.rxy.duration = ## INSERT VALUE HERE
[37]:
# Compiling the schedule to see the compiled instructions and pulse diagram

comp_sched = hw_agent.compile(sched)

Let us view the compiled Q1ASM instructions

[38]:
# Uncomment to display the compiled Q1ASM instructions and QCoDeS setting for each module

# hw_agent.latest_compiled_schedule.compiled_instructions

Looking at the Pulse Diagram

[39]:
# Uncomment to see the pulse diagram
comp_sched.plot_pulse_diagram(plot_backend="plotly")

T1 relaxation time (bit-flip time)#

The relaxation of the excited state to the ground state is a probabilistic process modeled as an exponential decay of the excited population with time. T1 reflects the characteristic time constant of the exponential decay. In essence, after \(t=T1\) 63% of the experiments have decayed to the ground state, after \(t=2 \cdot T1\) 86% has decayed, and 95% after \(t = 3 \cdot T1\). Generally, qubits are considered completely relaxed after 3-5 T1 times.

[40]:
repetitions = 2000

wait_start = 100e-9
wait_end = 10e-6
wait_npoints = 100

duration_drive = qubit.rxy.duration
amp_drive = qubit.rxy.amp180

# Initialize the schedule with the specified number of repetitions
sched = Schedule("T1")

# Add an initial optical reset pulse
sched.add(SquarePulse(amp=amp_laser, duration=duration_laser, port=port_laser, clock=clock_laser))

with (
    sched.loop(linspace(0, repetitions, repetitions, DType.NUMBER)),
    sched.loop(
        linspace(start=wait_start, stop=wait_end, num=wait_npoints, dtype=DType.TIME),
        strategy=LoopStrategy.UNROLLED,
    ) as wait_time,
):
    ## Microwave drive pulse
    ## Choose Square or Hermite Pulse for drive
    drive_pulse = sched.add(
        SquarePulse(amp=amp_drive, duration=duration_drive, port=port_drive, clock=clock_drive)
    )
    # drive_pulse = sched.add(SkewedHermitePulse(duration=duration_drive, amplitude=amp_drive, skewness=0, phase=0, port=port_drive, clock=clock_drive))
    # Marker Pulse to trigger scope
    sched.add(
        MarkerPulse(duration=duration_drive, port="qe0:marker"),
        ref_op=drive_pulse,
        ref_pt="start",
        rel_time=0,
    )

    ## Relaxation time
    op = sched.add(IdlePulse(duration=wait_time))

    ## Measure
    sched.add(
        Measure(
            qubit.name,
            coords={"wait": wait_time},
            acq_protocol="TriggerCount",
            bin_mode=BinMode.AVERAGE_APPEND,
        ),
        ref_op=op,
        rel_time=0,
        ref_pt="end",
    )

    ## Optical relaxation time
    sched.add(IdlePulse(duration=500e-9))
/tmp/ipykernel_9284/771276675.py:14: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

/tmp/ipykernel_9284/771276675.py:26: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

[41]:
# # Execute the experiment

T1_dataset = hw_agent.run(sched)
[42]:
# Once again seeing the dataset
T1_dataset
[42]:
<xarray.Dataset> Size: 3kB
Dimensions:            (acq_index_0: 100)
Coordinates:
  * acq_index_0        (acq_index_0) int64 800B 0 1 2 3 4 5 ... 95 96 97 98 99
    wait               (acq_index_0) float64 800B 1e-07 2e-07 ... 9.9e-06 1e-05
    loop_repetition_0  (acq_index_0) float64 800B 0.0 0.0 0.0 ... 0.0 0.0 0.0
Data variables:
    0                  (acq_index_0) float64 800B -1.0 -1.0 -1.0 ... -1.0 -1.0
Attributes:
    tuid:     20260415-121245-155-f7af7b
[43]:
# Compiling the schedule to see the compiled instructions and pulse diagram

# comp_sched = hw_agent.compile(sched)

Let us view the compiled Q1ASM instructions

[44]:
# Uncomment to display the compiled Q1ASM instructions and QCoDeS setting for each module

# hw_agent.latest_compiled_schedule.compiled_instructions

Looking at the Pulse Diagram

[45]:
# Uncomment to see the pulse diagram
# comp_sched.plot_pulse_diagram(plot_backend="plotly")

Ramsey (T2*) experiment#

The Ramsey experiment (or Ramsey interferometry) is used to measure the stability of a quantum state and as well the precise drive frequency. While T1 characterizes noise coupled to the qubit along the X and Y axes (transverse noise), T2 experiments characterize noise coupled along the Z axis (longitudnal noise). This decoherence is also modeled as an exponential decay, albeit with a different time constant than the T1 time constant. We slightly de-tune the carrier frequency of the drive pulses from the previously measured qubit frequency for convenience sake, so that the resulting Ramsey measurement oscillates.

[46]:
# Initialize the schedule with the specified number of repetitions
sched = Schedule("T2* Ramsey")

repetitions = 2000

# We detune the drive frequency slightly to observe better oscillations in the result
freq_detuning = 10e3  # Hz

wait_start = 20e-9
wait_end = 1020e-9
wait_npoints = 101

# Detuning the microwave frequency
sched.add(
    SetClockFrequency(clock=clock_drive, clock_freq_new=qubit.clock_freqs.spec - freq_detuning)
)

# Add an initial optical reset pulse
sched.add(SquarePulse(amp=amp_laser, duration=duration_laser, port=port_laser, clock=clock_laser))

with (
    sched.loop(linspace(0, repetitions, repetitions, DType.NUMBER)),
    sched.loop(
        linspace(start=wait_start, stop=wait_end, num=wait_npoints, dtype=DType.TIME),
        strategy=LoopStrategy.UNROLLED,
    ) as wait_time,
):
    ## First pi/2 pulse
    ## Choose Square or Hermite Pulse for drive
    drive_pulse_1 = sched.add(
        SquarePulse(amp=amp_drive, duration=duration_drive / 2, port=port_drive, clock=clock_drive)
    )
    # drive_pulse = sched.add(SkewedHermitePulse(duration=duration_drive/2, amplitude=amp_drive, skewness=0, phase=0, port=port_drive, clock=clock_drive))

    ## Add a wait time before playing the second pi/2 pulse, allowing the state to decohere (relaxation time)
    sched.add(IdlePulse(duration=wait_time))

    ## Second pi/2 pulse
    drive_pulse_2 = sched.add(
        SquarePulse(amp=amp_drive, duration=duration_drive / 2, port=port_drive, clock=clock_drive)
    )
    # drive_pulse = sched.add(SkewedHermitePulse(duration=duration_drive/2, amplitude=amp_drive, skewness=0, phase=0, port=port_drive, clock=clock_drive))

    # Marker Pulse to trigger scope
    sched.add(
        MarkerPulse(duration=duration_drive + wait_time, port="qe0:marker"),
        ref_op=drive_pulse_1,
        ref_pt="start",
        rel_time=0,
    )

    ## Measure
    sched.add(
        Measure(
            qubit.name,
            coords={"wait": wait_time},
            acq_protocol="TriggerCount",
            bin_mode=BinMode.AVERAGE_APPEND,
        ),
        ref_op=drive_pulse_2,
        rel_time=0,
        ref_pt="end",
    )

    ## Optical relaxation time
    sched.add(IdlePulse(duration=500e-9))
/tmp/ipykernel_9284/2571424324.py:15: FutureWarning:

clock_freq_new is deprecated as an argument to SetClockFrequency and will be removed in qblox-scheduler >= 2.0; use frequency instead.

/tmp/ipykernel_9284/2571424324.py:19: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

/tmp/ipykernel_9284/2571424324.py:31: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

/tmp/ipykernel_9284/2571424324.py:40: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

[47]:
# # Execute the experiment

T2_dataset = hw_agent.run(sched)
[48]:
# Once again seeing the dataset
T2_dataset
[48]:
<xarray.Dataset> Size: 3kB
Dimensions:            (acq_index_0: 101)
Coordinates:
  * acq_index_0        (acq_index_0) int64 808B 0 1 2 3 4 5 ... 96 97 98 99 100
    wait               (acq_index_0) float64 808B 2e-08 3e-08 ... 1.02e-06
    loop_repetition_0  (acq_index_0) float64 808B 0.0 0.0 0.0 ... 0.0 0.0 0.0
Data variables:
    0                  (acq_index_0) float64 808B -1.0 -1.0 -1.0 ... -1.0 -1.0
Attributes:
    tuid:     20260415-121258-555-0a7dd7
[49]:
# Compiling the schedule to see the compiled instructions and pulse diagram

# comp_sched = hw_agent.compile(sched)

Let us view the compiled Q1ASM instructions

[50]:
# Uncomment to display the compiled Q1ASM instructions and QCoDeS setting for each module

# hw_agent.latest_compiled_schedule.compiled_instructions

Looking at the Pulse Diagram

[51]:
# Uncomment to see the pulse diagram
# comp_sched.plot_pulse_diagram(plot_backend="plotly")

Hahn Echo (T2) Schedule#

The Ramsey experiment is sensitive to in-homogenous broadening; i.e., it is quite sensitive to low-frequency (1/f) noise. To counter this, we perform the Hahn Echo experiment, which makes the qubit less sensitive to such noise via dynamical decoupling. The only difference from the Ramsey experiment is that we apply an extra pi-pulse to refocus the noise due to pure dephasing.

[52]:
# Initialize the schedule with the specified number of repetitions
sched = Schedule("T2 Hahn Echo")

repetitions = 2000

# Note that there are 2 wait now, 1 after the first pi/2 pulse and one after the pi pulse, before the second pi/2 pulse.
wait_start = 20e-9
wait_end = 520e-9
wait_npoints = 51

# Detuning the microwave frequency
sched.add(
    SetClockFrequency(clock=clock_drive, clock_freq_new=qubit.clock_freqs.spec - freq_detuning)
)

# Add an initial optical reset pulse
sched.add(SquarePulse(amp=amp_laser, duration=duration_laser, port=port_laser, clock=clock_laser))

with (
    sched.loop(linspace(0, repetitions, repetitions, DType.NUMBER)),
    sched.loop(
        linspace(start=wait_start, stop=wait_end, num=wait_npoints, dtype=DType.TIME),
        strategy=LoopStrategy.UNROLLED,
    ) as wait_time,
):
    ## First pi/2 pulse
    ## Choose Square or Hermite Pulse for drive
    drive_pulse_1 = sched.add(
        SquarePulse(amp=amp_drive, duration=duration_drive / 2, port=port_drive, clock=clock_drive)
    )
    # drive_pulse = sched.add(SkewedHermitePulse(duration=duration_drive/2, amplitude=amp_drive, skewness=0, phase=0, port=port_drive, clock=clock_drive))

    ## Add a wait time before playing the second pi/2 pulse, allowing the state to decohere (relaxation time)
    sched.add(IdlePulse(duration=wait_time))

    ## Choose Square or Hermite Pulse for drive
    pi_pulse = sched.add(
        SquarePulse(amp=amp_drive, duration=duration_drive, port=port_drive, clock=clock_drive)
    )
    # drive_pulse = sched.add(SkewedHermitePulse(duration=duration_drive, amplitude=amp_drive, skewness=0, phase=0, port=port_drive, clock=clock_drive))

    ## Add a wait time before playing the second pi/2 pulse, allowing the state to decohere (relaxation time)
    sched.add(IdlePulse(duration=wait_time))

    ## Second pi/2 pulse
    drive_pulse_2 = sched.add(
        SquarePulse(amp=amp_drive, duration=duration_drive / 2, port=port_drive, clock=clock_drive)
    )
    # drive_pulse = sched.add(SkewedHermitePulse(duration=duration_drive/2, amplitude=amp_drive, skewness=0, phase=0, port=port_drive, clock=clock_drive))

    # Marker Pulse to trigger scope
    sched.add(
        MarkerPulse(duration=2 * duration_drive + 2 * wait_time, port="qe0:marker"),
        ref_op=drive_pulse_1,
        ref_pt="start",
        rel_time=0,
    )

    ## Measure
    sched.add(
        Measure(
            qubit.name,
            coords={"wait": wait_time},
            acq_protocol="TriggerCount",
            bin_mode=BinMode.AVERAGE_APPEND,
        ),
        ref_op=drive_pulse_2,
        rel_time=0,
        ref_pt="end",
    )

    ## Optical relaxation time
    sched.add(IdlePulse(duration=500e-9))
/tmp/ipykernel_9284/3439815431.py:13: FutureWarning:

clock_freq_new is deprecated as an argument to SetClockFrequency and will be removed in qblox-scheduler >= 2.0; use frequency instead.

/tmp/ipykernel_9284/3439815431.py:17: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

/tmp/ipykernel_9284/3439815431.py:29: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

/tmp/ipykernel_9284/3439815431.py:38: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

/tmp/ipykernel_9284/3439815431.py:47: FutureWarning:

amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.

[53]:
# # Execute the experiment

echo_dataset = hw_agent.run(sched)
[54]:
# Once again seeing the dataset
echo_dataset
[54]:
<xarray.Dataset> Size: 2kB
Dimensions:            (acq_index_0: 51)
Coordinates:
  * acq_index_0        (acq_index_0) int64 408B 0 1 2 3 4 5 ... 46 47 48 49 50
    wait               (acq_index_0) float64 408B 2e-08 3e-08 ... 5.2e-07
    loop_repetition_0  (acq_index_0) float64 408B 0.0 0.0 0.0 ... 0.0 0.0 0.0
Data variables:
    0                  (acq_index_0) float64 408B -1.0 -1.0 -1.0 ... -1.0 -1.0
Attributes:
    tuid:     20260415-121305-771-9adc3b
[55]:
# Compiling the schedule to see the compiled instructions and pulse diagram

# comp_sched = hw_agent.compile(sched)

Let us view the compiled Q1ASM instructions

[56]:
# Uncomment to display the compiled Q1ASM instructions and QCoDeS setting for each module

# hw_agent.latest_compiled_schedule.compiled_instructions

Looking at the Pulse Diagram

[57]:
# Uncomment to see the pulse diagram
# comp_sched.plot_pulse_diagram(plot_backend="plotly")