# QUA embedded in a Python (1)importqm.quaasqua# (2)!fromqmimportQuantumMachinesManagerfromOPX1000configurationimportconfig,qop_ip,cluster_name,cloud_username,cloud_password# or OPXplusconfiguration, depending on your hardware#################### The QUA program ####################withqua.program()ascw_output:# (3)!withqua.infinite_loop_():qua.play("cw","qubit")###################################### Open Communication with the QOP ######################################simulate=Truesimulation_in_cloud=Trueifsimulate:ifsimulation_in_cloud:client=QmSaas(email=cloud_username,password=cloud_password)instance=client.simulator(QoPVersion.v3_2_0)instance.spawn()qmm=QuantumMachinesManager(host=instance.host,port=instance.port,connection_headers=instance.default_connection_headers)else:qmm=QuantumMachinesManager(host=qop_ip,cluster_name=cluster_name)simulation_config=SimulationConfig(duration=1_000)# duration is in units of clock cycles, i.e., 4 nanosecondsjob=qmm.simulate(config,cw_output,simulation_config)job.get_simulated_samples().con1.plot()ifsimulation_in_cloud:instance.close()plt.show()else:qm=qmm.open_qm(config)job=qm.execute(cw_output)
QUA is a unique programming language and its first Domain Specific Language (DSL) is designed for Python. We chose Python due to its popularity in the Physics field, the fact that is open-source, its ability to easily connect with various lab instruments, and its large flexibility to operate a wide variety of tasks.
In line with Python's principle of modularity, QUA operates as a distinct package that can be imported when required. Essentially, QUA works as an API.
We use Python context managers with QUA to create a dedicated context for interpreting QUA instructions. For instance, the line with qua.program() as cw_output: establishes this context. Within this context, QUA-specific commands like qua.play() are interpreted correctly. Instructions within the indentation under this line are stored in a Python object, which the variable cw_output references for later use.
The QMM object acts as the gateway to the QM platform, facilitating communication when you interact with QMs and QUA programs.
importqm.quaasquafromqmimportQuantumMachinesManager# (1)!fromOPX1000configurationimportconfig,qop_ip,cluster_name,cloud_username,cloud_password# or OPXplusconfiguration, depending on your hardware#################### The QUA program ####################withqua.program()ascw_output:withqua.infinite_loop_():qua.play("cw","qubit")###################################### Open Communication with the QOP ######################################simulate=Truesimulation_in_cloud=Trueifsimulate:ifsimulation_in_cloud:client=QmSaas(email=cloud_username,password=cloud_password)instance=client.simulator(QoPVersion.v3_2_0)instance.spawn()qmm=QuantumMachinesManager(host=instance.host,port=instance.port,connection_headers=instance.default_connection_headers)else:qmm=QuantumMachinesManager(host=qop_ip,cluster_name=cluster_name)# (2)!simulation_config=SimulationConfig(duration=1_000)# duration is in units of clock cycles, i.e., 4 nanosecondsjob=qmm.simulate(config,cw_output,simulation_config)job.get_simulated_samples().con1.plot()ifsimulation_in_cloud:instance.close()plt.show()else:qm=qmm.open_qm(config)job=qm.execute(cw_output)
The code line initializes the QuantumMachinesManager class, setting up a communication link between your computer and your quantum machines. Essentially, it acts as a bridge between the quantum machines and your QUA-written programs. A single QuantumMachinesManager can connect to multiple quantum machines. Host refers to the OPX or router's IP depending in the network configuration. Port was used in early QOP 2.X versions, and it is not used anymore in QOP > 2.2.X, and QOP 3.X which are related to the OPX1000. Cluster_name refers to the name of the cluster in the web admint panel. Octave receives as an argument the informaiton related to the octave such as its IP and it is deprecated in QOP > 2.4.X.
The QM object takes your experimental setup details from the configuration dictionary. It's created by the QMM, and you supply it with a QUA program to execute.
importqm.quaasquafromqmimportQuantumMachinesManagerfromOPX1000configurationimportconfig,qop_ip,cluster_name,cloud_username,cloud_password# or OPXplusconfiguration, depending on your hardware#################### The QUA program ####################withqua.program()ascw_output:withqua.infinite_loop_():qua.play("cw","qubit")###################################### Open Communication with the QOP ######################################simulate=Truesimulation_in_cloud=Trueifsimulate:ifsimulation_in_cloud:client=QmSaas(email=cloud_username,password=cloud_password)instance=client.simulator(QoPVersion.v3_2_0)instance.spawn()qmm=QuantumMachinesManager(host=instance.host,port=instance.port,connection_headers=instance.default_connection_headers)else:qmm=QuantumMachinesManager(host=qop_ip,cluster_name=cluster_name)simulation_config=SimulationConfig(duration=1_000)# duration is in units of clock cycles, i.e., 4 nanosecondsjob=qmm.simulate(config,cw_output,simulation_config)job.get_simulated_samples().con1.plot()ifsimulation_in_cloud:instance.close()plt.show()else:qm=qmm.open_qm(config)# (1)!job=qm.execute(cw_output)
A quantum machine represents all experimental components like OPXs, Octaves, and your device. Within the QM Platform, you run experiments on these quantum machines. You can have multiple quantum machines from one OPX+/OPX1000 or one quantum machine from several OPX+/OPX1000s, called clusters. Each quantum machine's details are in the configuration dictionary. In Python, it's created with qmm.open_qm(config). Find here for OPX+ and here for OPX1000 the reference to the quantum machine API.
The configuration dictionary provides settings for the QMM to create a QM object. Once set, in the majority of cases, this dictionary remains constant between program's executions, and is commonly updated in between experiments. Typically, a unique configuration dictionary corresponds to each QM. However, one dictionary can be used for several QUA programs.
importqm.quaasquafromqmimportQuantumMachinesManagerfromOPX1000configurationimportconfig,qop_ip,cluster_name,cloud_username,cloud_password# (1)!#################### The QUA program ####################withqua.program()ascw_output:withqua.infinite_loop_():qua.play("cw","qubit")###################################### Open Communication with the QOP ######################################simulate=Truesimulation_in_cloud=Trueifsimulate:ifsimulation_in_cloud:client=QmSaas(email=cloud_username,password=cloud_password)instance=client.simulator(QoPVersion.v3_2_0)instance.spawn()qmm=QuantumMachinesManager(host=instance.host,port=instance.port,connection_headers=instance.default_connection_headers)else:qmm=QuantumMachinesManager(host=qop_ip,cluster_name=cluster_name)simulation_config=SimulationConfig(duration=1_000)# duration is in units of clock cycles, i.e., 4 nanosecondsjob=qmm.simulate(config,cw_output,simulation_config)# (2)!job.get_simulated_samples().con1.plot()ifsimulation_in_cloud:instance.close()plt.show()else:qm=qmm.open_qm(config)# (2)!job=qm.execute(cw_output)
For better organization, we store the configuration dictionary in a separate Python file named configuration.py, which we then import for use.
For a concise yet comprehensive overview, please consult our documentation.
The compiler receives the QM and QUA program objects, and translate QUA code into instructions to be received by the PPU.
fromqmimportQuantumMachinesManagerimportqm.quaasquafromOPX1000configurationimportconfig,qop_ip,cluster_name,cloud_username,cloud_password# or OPXplusconfiguration, depending on your hardware#################### The QUA program ####################withqua.program()ascw_output:withqua.infinite_loop_():qua.play("cw","qubit")###################################### Open Communication with the QOP ######################################simulate=Truesimulation_in_cloud=Trueifsimulate:ifsimulation_in_cloud:client=QmSaas(email=cloud_username,password=cloud_password)instance=client.simulator(QoPVersion.v3_2_0)instance.spawn()qmm=QuantumMachinesManager(host=instance.host,port=instance.port,connection_headers=instance.default_connection_headers)else:qmm=QuantumMachinesManager(host=qop_ip,cluster_name=cluster_name)simulation_config=SimulationConfig(duration=1_000)# duration is in units of clock cycles, i.e., 4 nanosecondsjob=qmm.simulate(config,cw_output,simulation_config)job.get_simulated_samples().con1.plot()ifsimulation_in_cloud:instance.close()plt.show()else:qm=qmm.open_qm(config)job=qm.execute(cw_output)# (1)!
The first step in qm.execute(cw_output) is that the QUA object is send over the ethernet to your system and received by the compiler. Given that QUA is a specialized programming language tailored to our technology, standard computer CPUs cannot process it directly. Instead, our compiler inside the OPX+/OPX1000 converts a QUA object (such as cw_output from line 10) into instructions for the Pulse Processing Unit. It allows users to code quantum experiments in the user-friendly QUA language, while utilizing the power of the Pulse Processing Unit's low-level advanced commands tailored for quantum experiments.
The PPU is located at the receiving end of the QM platform, after compilation. It serves as the physical layer, managing the pulses sent to and from your device. Note that the PPU instructions processing time is fixed and determinisitc and is independent of the input to the instructions.
fromqmimportQuantumMachinesManagerimportqm.quaasquafromOPX1000configurationimportconfig,qop_ip,cluster_name,cloud_username,cloud_password# or OPXplusconfiguration, depending on your hardware#################### The QUA program ####################withqua.program()ascw_output:withqua.infinite_loop_():qua.play("cw","qubit")###################################### Open Communication with the QOP ######################################simulate=Truesimulation_in_cloud=Trueifsimulate:ifsimulation_in_cloud:client=QmSaas(email=cloud_username,password=cloud_password)instance=client.simulator(QoPVersion.v3_2_0)instance.spawn()qmm=QuantumMachinesManager(host=instance.host,port=instance.port,connection_headers=instance.default_connection_headers)else:qmm=QuantumMachinesManager(host=qop_ip,cluster_name=cluster_name)simulation_config=SimulationConfig(duration=1_000)# duration is in units of clock cycles, i.e., 4 nanosecondsjob=qmm.simulate(config,cw_output,simulation_config)job.get_simulated_samples().con1.plot()ifsimulation_in_cloud:instance.close()plt.show()else:qm=qmm.open_qm(config)job=qm.execute(cw_output)# (1)!
After compilation, the instructions are passed to the Pulse Processing Unit (PPU). The PPU is a special processor designed for quantum experiments. It's activated by loading a unique bitfile for the system's FPGA. Thanks to this stationary bitfile and the compiler, you can quickly set up and run quantum experiments without reloading a new bitfile for each new quantum sequence. When you write a QUA program, you're outlining steps for the PPU to follow. The PPU send pulses as directed by qua.play(), as well as use its instruction set to do with qua.infinite_loop(): (line 11).
The simulator and cloud simulator in your system works wih the same compiled instructions as the PPU and mimics the PPU's operations. This makes it an invaluable tool for debugging.
fromqmimportQuantumMachinesManagerimportqm.quaasquafromOPX1000configurationimportconfig,qop_ip,cluster_name,cloud_username,cloud_password# or OPXplusconfiguration, depending on your hardware#################### The QUA program ####################withqua.program()ascw_output:withqua.infinite_loop_():qua.play("cw","qubit")###################################### Open Communication with the QOP ######################################simulate=Truesimulation_in_cloud=Trueifsimulate:ifsimulation_in_cloud:client=QmSaas(email=cloud_username,password=cloud_password)# (1)!instance=client.simulator(QoPVersion.v3_2_0)instance.spawn()qmm=QuantumMachinesManager(host=instance.host,port=instance.port,connection_headers=instance.default_connection_headers)else:qmm=QuantumMachinesManager(host=qop_ip,cluster_name=cluster_name)simulation_config=SimulationConfig(duration=1_000)# (2)!job=qmm.simulate(config,cw_output,simulation_config)# (3)!job.get_simulated_samples().con1.plot()# (4)!ifsimulation_in_cloud:instance.close()# (5)!plt.show()else:qm=qmm.open_qm(config)job=qm.execute(cw_output)
Client is the instantiation of the class that connects to the cloud simulator.
This line of code creates the SimulationConfig object. Each duration unit is a clock cycle, equivalent to 4 nanoseconds. Here we define the simulation to las 1000 clock cycles (4 nanoseconds). The LoopbackInterface is used to simulate physical connections between (3,4) outputs and (1,2) inputs.
The QMM grants you access to the simulator, which needs three inputs: the configuration dictionary, the QUA program, and the SimulationConfig object. Simulating the PPU is slower than running the QUA program directly on the PPU. Additionally, the simulation doesn't interfere with the PPU's execution of QUA programs.
The simulated job also has an API. For example, with this line you can plot the simulation samples and observe the pulse sequence that you have programmed.
The Cloud Simulator instance should be closed once the simulation is completed to free the resource. To dig deeper into the lifetime of the instance follow this link.
QM platform
Upon executing the Python file, the QMM object is created, thereby establishing communication. Using the configuration dictionary, the QM object is instantiated by the QMM in line 21. The QUA program object is then sent over the network to be compiled. Once compiled, the program is executed by the PPU.
In the QUA program, we've directed the system to operate an infinite loop. This means it will repeatedly execute the qua.play() command indefinitely.
Note: The execution interface to OPX+/OPX1000 is a-synchronous, allowing the client CPU to perform other tasks in the Python console while the QUA program is executed in the OPX.
Below you will find how to identify the link between qua.play('cw', 'qubit') and the configuration dictionary.
Configuration dictionary
The controllers dictionary contains information about which analog and digital outputs as well as analog inputs are under usage by the QM. In continuous_signal.py, qubit will be sending signals from analog output 2.
config={"version":1,"controllers":{'con1':{# (1)!"type":"opx1000","fems":{1:{# The keyword "band" refers to the following frequency bands:# 1: (50 MHz - 5.5 GHz)# 2: (4.5 GHz - 7.5 GHz)# 3: (6.5 GHz - 10.5 GHz)# Note that the "coupled" ports O1 & I1, O2 & O3, O4 & O5, O6 & O7, O8 & O1# must be in the same band, or in bands 1 & 3.# The keyword "full_scale_power_dbm" is the maximum power of# normalized pulse waveforms in [-1,1]. To convert to voltage,# power_mw = 10**(full_scale_power_dbm / 10)# max_voltage_amp = np.sqrt(2 * power_mw * 50 / 1000)# amp_in_volts = waveform * max_voltage_amp# ^ equivalent to OPX+ amp# Its range is -41dBm to +10dBm with 3dBm steps."type":"MW","analog_outputs":{# (2)!1:{"full_scale_power_dbm":-10,# units of dBm"band":2,"upconverter_frequency":7e9,# units of Hz},# resonator2:{"full_scale_power_dbm":0,"band":2,"upconverter_frequency":5e9},# qubit},"digital_outputs":{},"analog_inputs":{1:{"band":2,"downconverter_frequency":7e9},# I from down-conversion},},2:{"type":"LF","analog_outputs":{# Flux line1:{"offset":0.0,# The "output_mode" can be used to tailor the max voltage and frequency bandwidth, i.e.,# "direct": 1Vpp (-0.5V to 0.5V), 750MHz bandwidth (default)# "amplified": 5Vpp (-2.5V to 2.5V), 330MHz bandwidth# Note, 'offset' takes absolute values, e.g., if in amplified mode and want to output 2.0 V, then set "offset": 2.0"output_mode":"direct",# The "sampling_rate" can be adjusted by using more FEM cores, i.e.,# 1 GS/s: uses one core per output (default)# 2 GS/s: uses two cores per output# NOTE: duration parameterization of arb. waveforms, sticky elements and chirping# aren't yet supported in 2 GS/s."sampling_rate":1e9,# At 1 GS/s, use the "upsampling_mode" to optimize output for# modulated pulses (optimized for modulated pulses): "mw" (default)# unmodulated pulses (optimized for clean step response): "pulse""upsampling_mode":"pulse",},},"digital_outputs":{1:{},},},},},},}
The con1 dictionary details the usage of analog front-end ports for constructing the quantum machine. You can refer to the hardware specifications here for OPX+ and here for OPX1000. In our experiment involving a single qubit and a coupled resonator, we've defined 2 analog outputs and 1 analog input from the MW-FEM.
Within analog_outputs, you specify the analog output ports you intend to use. These ports will be associated with an element. For the qubit element dictionary, we'll link analog output 2.
The qubit that you work with during the QUA program belongs to the Elements dictionary. Once the config gets loaded into the quantum machine, qubit has all the information of connectivity, pulses to be played, intermediate frequency working with, etc.
This line marks the start of the elements dictionary. Elements are objects you use in writing QUA programs, for example to send a pulse to an element you can use qua.play('cw', 'qubit').
This line represents the name of the element and your are free to change it to suit your experiments.
The MWInput dictionary contains information on the connection between the analog outputs and the element.
The first argument points to the controller (OPX1000 or OPX+) you are making a connection to, the second refers to which FEM you are pointing to, and last the third arguemnt points to analog_output port.
This variable is the intermediate frequency at which the pulse cw is output by qua.play('cw', 'qubit').
The string cw is the operation we used in qua.play('cw', 'qubit'), and it can be any name that better fits your experiment. The string after the colon is the pulse assocaited with the operation.
The operation cw we used in qua.play() send the information connected to const_pulse in the Pulses dictionary. The const_pulse contains the shape and duration of the operation cw.
This lines marks the beginning of the pulses dictionary.
The string const_pulse is the name of the pulse and can be changed to fit your experimental needs.
The variable const_len defines the pulse duration. It needs to be a multiple of 4, and cannot be smaller than 16 ns. The units are nanoseconds.
The waveforms sub-dictionary contains the waveforms associated with const_pulse. Note that this waveforms are not what is played at the analog outputs but the input to the CSF matrices, which refer to the Analog Waveform Manipulations.
The const_pulse has associated to it const_wf and zero_wf located in the Waveforms dictionary. These waveforms combined with the qubit's intermediate frequency send the desire signal.
This lines marks the beginning of the waveforms dictionary.
The name is describe by the string, and it can modified to any string that fits your set up better. In this example, we are looking at constant waveform, that is, of constant amplitude, which is given by const_amp.
Digital Pulse
To synchronize a digital pulse with the qua.play('cw', 'qubit') command to activate an RF switch, please refer to our documentation on digital waveform manipulations. If you need to send a digital pulse without any attached analog outputs to trigger another lab instrument externally, consult our guide on digital inputs.
We would like to encourage you to get your hands dirty by trying out simulation or execution of the above QUA program.
As a reminder:
You will re-use the same configuration file that you downloaded in the introduction section. As a matter of fact, the workflow between
the configuration dictionary and experiments is have a single dictionary that remains semi-static and it is updated in between QUA
programs.
If you are working with a physical OPX unit please update to the their relevant values the qop_ip and cluster_name correspondingly.
Alternatively, when using the Cloud Simulator please update with your credentials the values of cloud_username and cloud_password in the configuration file.
Note that, when pursuing the execution of the program on the hardware with qm.execute() you will need an oscilloscope to visualize the
instrument output.
Now let's proceed:
Copy paste the above program in this section and into a file that you can name continuous_signal.py.
In the upcoming module, you'll delve into concepts like waveform acquisition, data flow, and more, all within the context of calibrating your system's analog inputs.