ZZ Coupling¶
Prerequisites¶
This guide assumes you have a configured DeviceSetup as well as Qubit objects with assigned parameters. Before you can run the ZZ Coupling experiment, you need to have tuned up the qubit drive. In this guide, we assume that these tune-up steps have been performed. Please see our tutorials if you need to create your setup and qubits for the first time.
You can run this notebook on real hardware in the lab. However, if you don't have the hardware at your disposal, you can also run the notebook "as is" using an emulated session (see below).
If you are just getting started with the LabOne Q Applications Library, please don't hesitate to reach out to us at info@zhinst.com.
Background¶
In this guide, you will learn how to characterize a ZZ coupling between a pair of qubits mediated by a tunable coupler and use this information to set a bias to the coupler that maximize the gate contrast. This will be performed using the zz_coupling_strength experiment available in the LabOne Q Applications Library. Residual coupling between qubits during idle times is a significant source of leakage errors. The challenge is to develop a coupling scheme that allows low coupling in idle time and strong coupling when the gate is applied, to have both small leakage error and high gate fidelity. Let's take for example a ZZ tunable interaction described by the Hamiltonian:
$$H_{\text{eff}}/\hbar = \frac{1}{2}\sum_{i=1,2}\left(\omega_i + \frac{\alpha_\text{ZZ}}{2}\right)\sigma_\text{z}^{(i)} + \frac{\alpha_\text{ZZ}}{4} \sigma_\text{z}^{(1)}\sigma_\text{z}^{(2)}.$$
In this expression, $\omega_{1,2}$ are the qubit frequencies, $\sigma_{\text{ZZ}}$ is the tunable cross-Kerr ZZ-interaction rate, and $\sigma_\text{Z}^{(1),(2)}$ are the Pauli operators.
Following the scheme presented in the image below, we use the frequency of the coupler element as a control parameter to tune $\alpha_\text{ZZ}$. To characterize the dependency of this parameter on the coupler frequency, we can study the frequency of $Q_1$ in a modified echo experiment for both cases of $Q_2$ being in the ground and the excited state. The resulting frequency difference will correspond to the parameter $\alpha_\text{ZZ}$.
To change the frequency of the coupler, we apply a static magnetic flux $\phi$ and observe the frequency shift of $Q_1$, as illustrated above. This means that in our experiment we will sweep two parameters: the DC bias applied to the coupler, and the delay between two x90 to perform the modified Echo. The result will be a map between the bias applied and the frequency difference depending on the state of $Q_2$, which can be directly interpreted as the coupling between our two qubits. From here, one can set the DC bias where the coupling is minimal to guarantee smallest coupling in idle time, while reaching the maximum coupling using fast flux pulses during gates.
Imports¶
You'll start by importing laboneq.simple.
from laboneq.simple import *
Define your experimental setup¶
Let's define our experimental setup. We will need:
a set of TunableTransmonOperations
a QPU
Here, we will be brief. We will mainly provide the code to obtain these objects. To learn more, check out these other tutorials:
We will use 4 TunableTransmonQubits in this guide connected with a square topology.
number_of_qubits = 4
DeviceSetup¶
This guide requires a setup that can drive and readout at least two tunable transmon qubits mediated by a tunable coupler. Your setup could contain either an SHFQC+ instrument, or alternatively an SHFSG and an SHFQA instrument. Additionally, we will use an HDAWG to provide flux lines for our Qubit and TunableCoupler. Here, we will use an SHFQC+ with 6 signal generation channels, an HDAWG with 8 channels, and a PQSC.
If you have used LabOne Q before and already have a DeviceSetup for your setup, you can reuse that.
If you do not have a DeviceSetup, you can create one using the code below. Just change the device numbers to the ones in your rack and adjust any other input parameters as needed.
from laboneq.contrib.example_helpers.generate_device_setup import generate_device_setup
setup = generate_device_setup(
number_qubits=number_of_qubits,
pqsc=[{"serial" : "DEV10001"}],
shfqc=[{"serial" : "DEV12001",
"number_of_channels" : 6,
"readou_multiplex" : 6,
"options" : "SHFQC/PLUS/QC6CH/RTR"}],
hdawg=[{"serial" : "DEV8800",
"number_of_channels" : 8,
"options" : "HDAWG8/CNT/ME/PC"}],
include_flux_lines=True,
server_host="localhost",
)
Qubits¶
We will generate 4 TunableTransmonQubits from the logical signal groups in our DeviceSetup. The names of the logical signal groups, q0, q1, q2, q3 will be the UIDs of the qubits. Moreover, the qubits will have the same logical signal lines as the ones of the logical signal groups in the DeviceSetup.
from laboneq_applications.qpu_types.tunable_transmon import TunableTransmonQubit
qubits = TunableTransmonQubit.from_device_setup(setup)
for q in qubits:
print("-------------")
print("Qubit UID:", q.uid)
print("Qubit logical signals:")
for sig, lsg in q.signals.items():
print(f" {sig:<10} ('{lsg:>10}')")
Configure the qubit parameters to reflect the properties of the qubits on your QPU using the following code:
for q in qubits:
q.parameters.ge_drive_pulse["sigma"] = 0.25
q.parameters.readout_amplitude = 0.5
q.parameters.reset_delay_length = 1e-6
q.parameters.readout_range_out = -25
q.parameters.readout_lo_frequency = 7.4e9
qubits[0].parameters.drive_lo_frequency = 6.4e9
qubits[0].parameters.resonance_frequency_ge = 6.3e9
qubits[0].parameters.resonance_frequency_ef = 6.0e9
qubits[0].parameters.readout_resonator_frequency = 7.0e9
qubits[1].parameters.drive_lo_frequency = 6.4e9
qubits[1].parameters.resonance_frequency_ge = 6.5e9
qubits[1].parameters.resonance_frequency_ef = 6.3e9
qubits[1].parameters.readout_resonator_frequency = 7.3e9
qubits[2].parameters.drive_lo_frequency = 6.0e9
qubits[2].parameters.resonance_frequency_ge = 5.8e9
qubits[2].parameters.resonance_frequency_ef = 5.6e9
qubits[2].parameters.readout_resonator_frequency = 7.2e9
qubits[3].parameters.drive_lo_frequency = 6.0e9
qubits[3].parameters.resonance_frequency_ge = 5.5e9
qubits[3].parameters.resonance_frequency_ef = 5.3e9
qubits[3].parameters.readout_resonator_frequency = 7.5e9
Tunable Couplers¶
Let's now add tunable couplers to our setup! Since we want to name the couplers depending on the topology to keep things in order, instead of having an automatic setup, let's manually add the lines to the setup. We will see how these connections can be formalized later in our QPUTopology.
from laboneq_applications.qpu_types.tunable_coupler import TunableCoupler
# define desired couplings
couplings = {"c_q0q1" : ("q0", "q1"),
"c_q1q2" : ("q1", "q2"),
"c_q2q3" : ("q2", "q3"),
"c_q3q0" : ("q3", "q0")
}
# add signal lines for tunable couplers
for n, key in enumerate(couplings):
channel_id = number_of_qubits + n # first channels are already occupied by the flux line of TunableTransmon
if key in setup.logical_signal_groups:
print(f"INFO: Logical signal for group {key} is already in setup")
else:
setup.add_connections("hdawg_0", create_connection(to_signal=f"{key}/flux", ports=f"SIGOUTS/{channel_id}"))
Let's extract our list of couplers from the setup, now that the lines are added. Note that you now need to pass the keys for the coupler, otherwise the routine will try to also convert the qubit into couplers, and you will get an error since more logical signal lines than needed are present. Try it out!
couplers = TunableCoupler.from_device_setup(setup, qubit_uids=couplings.keys())
Quantum Operations¶
Create the set of TunableTransmonOperations. In this example we won't need any two qubit gates, as we will just tune the coupler DC bias without any 2-qubit gates. Hence, no specific further operation is needed. We leave this exercise for the next workflow.
from laboneq_applications.qpu_types.tunable_transmon import TunableTransmonOperations
qops = TunableTransmonOperations()
QPU¶
Now that we have all the ingredients, let's create the QPU object from the qubits, couplers and the quantum operations.
from laboneq.dsl.quantum import QPU
qpu = QPU(qubits+couplers, quantum_operations=qops)
QPU Topology¶
We now define the topology of this chip following the couplings defined before. This is helpful both for visualizing the qpu, and also to make automation much easier with our workflows. We add an edge in both directions to support the fact that the coupler can act in both directions.
for coupler, (q0, q1) in couplings.items():
qpu.topology.add_edge(source_node=q0, target_node=q1, quantum_element=coupler, tag="coupler")
qpu.topology.add_edge(source_node=q1, target_node=q0, quantum_element=coupler, tag="coupler")
Let's take a look at it by plotting the topology!
qpu.topology.plot();
Alternatively, load from a file¶
If you you already have a DeviceSetup and a QPU stored in .json files, you can simply load them back using the code below:
from laboneq import serializers
setup = serializers.load(full_path_to_device_setup_file)
qpu = serializers.load(full_path_to_qpu_file)
qubits = qpu.quantum_elements
qops = qpu.quantum_operations
Connect to Session¶
session = Session(setup)
session.connect(do_emulation=True) # do_emulation=False when at a real setup
Create a FolderStore for Saving Data¶
The experiment Workflows can automatically save the inputs and outputs of all their tasks to the folder path we specify when instantiating the FolderStore. Here, we choose the current working directory.
# import FolderStore from the `workflow` namespace of LabOne Q, which was imported
# from `laboneq.simple`
from pathlib import Path
folder_store = workflow.logbook.FolderStore(Path.cwd())
We disable saving in this guide. To enable it, simply run folder_store.activate().
folder_store.deactivate()
Optional: Configure the LoggingStore¶
You can also activate/deactivate the LoggingStore, which is used for displaying the Workflow logging information in the notebook; see again the tutorial on Recording Experiment Workflow Results for details.
Displaying the Workflow logging information is activated by default, but here we deactivate it to shorten the outputs, which are not very meaningful in emulation mode.
We recommend that you do not deactivate the Workflow logging in practice.
from laboneq.workflow.logbook import LoggingStore
logging_store = LoggingStore()
logging_store.deactivate()
Running the Experiment Workflow¶
You'll now instantiate the experiment workflow and run it. For more details on what experiment workflows are and what tasks they execute, see the Experiment Workflows tutorial.
You'll start by importing the ZZ coupling strength experiment workflow from laboneq_applications, as well as plot_simulation for inspecting the experiment sequence.
from laboneq.contrib.example_helpers.plotting.plot_helpers import plot_simulation
from laboneq_applications.contrib.experiments import zz_coupling_strength
Let's first create the options class for the QND measurement experiment and inspect it using the show_fields function from the workflow namespace of LabOne Q, which was imported from laboneq.simple:
options = zz_coupling_strength.experiment_workflow.options()
workflow.show_fields(options)
Here, let's disable closing the figures produced by the analysis so we see them in the cell output. Note however that the analysis routine in emulation mode will not be representative, because we do not acquire data from a real experiment.
options.count(2**13)
Now we run the experiment workflow on two pairs in parallel.
Notice some important differences compared to running a workflow for single qubit calibration:
- Qubit pairs are passed instead of single qubits in the format
[[q0, q1], ...] - Parallelization is not trivial for two qubit gates, as it is much easier to have a conflict of resources. Here we follow these rules:
- You cannot calibrate the same qubit for more than one pair, i.e. the qubit cannot appear more than once in the list.
- Each qubit pair passed needs to have an edge between them of type
couplerfor the workflow to start. - We follow the convention where the first qubit of the pair is the source node and the second is the target node. In this workflow, this means that the modified Echo experiment will be performed on
q0withq1as a spectator.
Below we left commented out some example of a disallowed workflow to let you test it. Check if the error makes sense!
import numpy as np
# qubit pairs
qubit_pairs = [["q0", "q1"], ["q2", "q3"]]
# Example on invalid inputs
## invalid qubit pair: invalid connections
# qubit_pairs = [["q0", "q2"]]
## invalid qubit pairs: valid connections, but conflicting resources
# qubit_pairs = [["q0", "q1"], ["q1", "q2"]]
exp_workflow = zz_coupling_strength.experiment_workflow(
session=session,
qpu=qpu,
qubit_pairs=qubit_pairs,
biases=[list(np.linspace(-0.06,0.06, 11)) for _ in range(len(qubit_pairs))],
delays=[list(np.linspace(0, 10e-6, 11)*(_+1)) for _ in range(len(qubit_pairs))],
options=options,
)
workflow_results = exp_workflow.run()
Inspect the Tasks that were Run¶
for t in workflow_results.tasks:
print(t)
Inspect the Output Simulation¶
You can also inspect the compiled experiment and plot the simulated output:
compiled_experiment = workflow_results.tasks["compile_experiment"].output
plot_simulation(compiled_experiment, length=50e-6)
Inspecting the Source Code of the Pulse-Sequence Creation Task¶
You can inspect the source code of the create_experiment to see how the experiment pulse sequence is created using quantum operations. To learn more about the latter, see the Quantum Operations tutorial.
zz_coupling_strength.create_experiment.src
To learn more about how to work with experiment Workflows, check out the Experiment Workflows tutorial.
Here, let's briefly inspect the analysis workflow results.
Analysis Results¶
Let's check what tasks were run as part of the analysis workflow:
analysis_workflow_results = workflow_results.tasks["analysis_workflow"]
for t in analysis_workflow_results.tasks:
print(t)
We can access the optimal voltage_offset extract by the analysis from the output of the analysis workflow. Since we are in emulation mode, the value will be the first one passed in the sweep.
from pprint import pprint
pprint(analysis_workflow_results.output)
Great! You've now run your first experiment based on 2 qubit topology! Check out other experiments in this manual to keep characterizing your qubits.