Get started with sqil-experiments
¶
Installation¶
Clone the github repository
git clone https://github.com/SQIL-EPFL/sqil-experiments.git
Navigate to the sqil-experiments
folder and install the dependencies using poetry.
If you still haven't installed poetry, pip install poetry poetry-plugin-shell
cd sqil-experiments
poetry install
poetry env activate
Separately install Qt bindings, which are needed for plottr to work and cannot be packaged with the other dependencies.
poetry run pip install PyQt5
Requirements for sqil-experimental
to work:
- A
config.yaml
file - A setup file
1. The config file¶
The config file (config.yaml
) it's located in the root directory of sqil-experiments
and must NOT be deleted or renamed.
It's currently used only to point to a setup file
This allows you to easily switch between different setups, which could be useful if two experiments share the same measurement computer. For example, let's say that experiment 1 requires a Yoko to control flux and experiment 2 doesn't. Create a setup file for each experiment, setup_1.py
and setup_2.py
in which you specify all the instruments connected to the line. Then specify which setup file to use based on what you need to measure.
2. The setup file¶
The setup file controls all the things related to your experimental setup
- Choose where the experimental data is saved
- Define the Zuirch Instruments setup object
- Generate a QPU file when it's not available (e.g. first time running an experiment)
- Define the instruments used by your experiments
- Change the default behavior of the instruments
IMPORTANT: The setup file is a python file, meaning that complex functionality can be defined and passed to the experiment.
2.1. Data storage¶
db_root
and db_root_local
are used to define the respective remote and local database directories.
data_folder_name
is used to name the specific data collection folder.
Data will be saved in the following directories:
db_path_local/data_folder_name/
db_path/data_folder_name/
We'll refer to these as 'data folders' or 'data paths'
The QPU file will be saved in both data folders with name specified by qpu_filename
.
import os
data_folder_name = "test"
# Data storage
db_root = r"C:\Users\sqil\Desktop\code\sqil-experiments\data"
db_root_local = r"C:\Users\sqil\Desktop\code\sqil-experiments\data_local"
storage = {
"db_type": "plottr",
"db_path": os.path.join(db_root, data_folder_name),
"db_path_local": os.path.join(db_root_local, data_folder_name),
"qpu_filename": "qpu.json",
}
2.2. Zuirch Instruments setup¶
A function that specifies how to generate the Zurich Instruments setup object. It's called every time is needed to connect to Zurich Instruments devices. This method is not required if you're not using ZI.
It's recmmended to construct this function using the generate_device_setup
provided by LaboneQ.
from laboneq.contrib.example_helpers.generate_device_setup import generate_device_setup
def generate_zi_setup():
return generate_device_setup(
number_qubits=1,
shfqc=[
{"serial": "dev12183", "number_of_channels": 4, "options": "SHFQC/QC4CH"}
],
include_flux_lines=False,
multiplex_drive_lines=True,
query_options=False,
)
2.3. Generate QPU¶
The QPU, quantum processing unit, contains all the information about your qubits.
The generate_qpu
function gives the experiment instructions on how to generate the QPU file, in case one it's not already available. This means generate_qpu
is supposed to run only once: when the first experiment is run.
You can avoid defining this function if you already have a QPU file. For example if you're re-measuring the same qubit on the same setup, just copy the old QPU file into the new data folder (db_path_local/data_folder_name/
)
It's recommended to use the from_device_setup
function if you have ZI in your setup.
NOTE: A QPU is required even if you're not using any device from Zurich Instruments.
from helpers.sqil_transmon.operations import SqilTransmonOperations
from helpers.sqil_transmon.qubit import SqilTransmon
from laboneq.dsl.quantum import QPU
def generate_qpu(zi_setup):
qubits = SqilTransmon.from_device_setup(zi_setup)
quantum_operations = SqilTransmonOperations()
qpu = QPU(qubits, quantum_operations)
# Set required qubit parameters
for qubit in qpu.quantum_elements:
qubit.update(
**{
"readout_lo_frequency": 7e9,
"drive_lo_frequency": 5e9,
}
)
return qpu
It's a good idea t define here the LO frequencies for readout and drive. The QPU is generated with these two values empty, but not experiment can start without the LOs.
2.4. Define the instruments¶
The instruments
dictionary contains all the information about your instruments.
Every instrument needs a type
, which is used by sqil-core
to cast it to the correct class and control it properly. If there can be multiple models of the same instrument type, also a model
is required.
In the example below the experimental setup is made of our Zurich Instruments and an SGS used as external LO.
Even if we created the generate_setup
function for ZI earlier, now we need to bind it to the dictionary entry.
For the SGS, we need to specify a type
and a model
, since we could use multiple instruments as LO sources.
The address
is required to connect to it, while the name
is a human readable string used for logs.
The variables
field can be used by some instruments quickly access variables present in your experiment context.
This is generally used to automatically allow sweeps on the instrument variables.
With the old experimental code you would control the LO through the parameter "ext_LO_freq". The new way is more abstract and the LO can be controlled by any variable present in the experiment (qubit paramenters, experiment options, etc.).
To bind a variable you need to specify a function that returns the value you want, given the experiment object.
instruments = {
"zi": {
"type": "ZI",
"address": "localhost",
"generate_setup": generate_zi_setup,
"generate_qpu": generate_qpu,
},
"lo": {
"type": "LO",
"model": "RohdeSchwarzSGS100A",
"name": "SGSA100",
"address": "TCPIP0::192.168.1.56::inst0::INSTR",
"variables": {
"frequency": lambda exp: exp.qpu.quantum_elements[0].parameters.external_lo_frequency,
"power": lambda exp: exp.qpu.quantum_elements[0].parameters.external_lo_power,
},
},
}
In the dictionary defining the instruments for your experiments, the key lo represents the variable name used to control the Local Oscillator (LO). In this case, the dictionary key "lo" is associated with the SGS, and when writing your experiment code, you interact with it as if it’s a generic LO object—regardless of its specific model. For example, you would call lo.set_frequency(11e9)
to set the frequency, no matter whether you're using an SGS or a different LO.
The reason this is useful is that the sqil-core framework generalizes all LOs, abstracting away the specific details of the underlying hardware. This means you don’t need to worry about treating the SGS as an SGS. Instead, you just treat it as an abstract LO object that you can control in a standardized way.
If, in the future, you decide to switch to a different LO source, e.g. a Signal Core, you don’t have to go through and modify every experiment script. You simply update the dictionary by changing the lo entry to reflect the new model and name, and your experiment code will continue working exactly the same way without needing any further adjustments.
2.5. Change instrument behavior¶
Some instruments have a default behavior, like turning on before the experiment and turning off after the experiment. You can change these behaviors or add new ones by overriding the instrument functions.
connect
: how the experiment should connect to the instrumentsetup
: how to setup the instrument after it's connected (e.g. turn on phase locking or set a specific power)before_experiment
: function that runs before the experiment startsbefore_sequence
: function that runs just before the pulse sequence is sentafter_sequence
: function that runs just after the pulse sequence is sentbefore_experiment
: function that runs after the experiment ends
The difference between before_experiment
and before_sequence
is that the first one runs only once, while the second one runs every time the pulse sequence is sent and can be used to handle sweeps.
NOTE: when overriding these functions you are overriding the instrument class default for that function, which means you have access to the self
argument and can access class attributes, like name
, address
, etc., and even the instrument's functions, like turn_on
, set_frequency
, etc.
NOTE: most instruments have default behaviors for these functions, before overriding them check what they're doing, because they may be handling something you didn't think about. Like safely handling connections or forcing some useful behaviors by default, like turning on phase locking.
def lo_after_experiment(self, *args, **kwargs):
print("Setting low power and turning off {self.name}")
self.set_power(-60)
self.turn_off()
instruments = {
"zi": {
"type": "ZI",
"address": "localhost",
"generate_setup": generate_zi_setup,
"generate_qpu": generate_qpu,
},
"lo": {
"type": "LO",
"model": "RohdeSchwarzSGS100A",
"name": "SGSA100",
"address": "TCPIP0::192.168.1.56::inst0::INSTR",
"variables": {
"frequency": lambda exp: (
exp.qpu.quantum_elements[0].parameters.external_lo_frequency
),
"power": lambda exp: (
exp.qpu.quantum_elements[0].parameters.external_lo_power
),
},
# Bind the new function to the instrument
"after_experiment": lo_after_experiment,
},
}
3. Run an experiment¶
import numpy as np
from time_rabi import TimeRabi, TimeRabiOptions
time_rabi = TimeRabi()
options = TimeRabiOptions()
options.count = 2**8
pulse_lengths = np.linspace(1e-10,600e-9, 53)
result = time_rabi.run(pulse_lengths, options=options)