Goal of the Experiment ====================== In this experiment, we want to measure Rabi oscillations – oscillations of the qubit state that are driven by a control signal. Assume that the qubit is initially in the ground state (state 0), a drive pulse is applied to rotate the qubit on the Bloch sphere around a rotation axis in the x-y plane. The qubit is then measured by calculating the effect of the resonator (that is coupled to the qubit) on a measurement pulse. The rotation angle, and consequently the probability to find the qubit in the excited state (1), depends on the amplitude of the drive pulse. The protocol is repeated with varying amplitudes (a). For each amplitude, the protocol is repeated many times for averaging, which allows extracting the probability of the qubit to be in the excited state after the drive pulse is applied. This probability is then plotted as a function of the drive amplitude, from which the rotation angle, as a function of the amplitude, can be extracted. This experiment provides an important tool for calibrating quantum gates. For example, the amplitude at which the qubit reaches a rotation of 180 degrees gives us the required amplitude for performing an X-gate (the quantum NOT gate). Similarly, we can run this program to identify the amplitude required to perform a π/2-rotation. System Setup ============ The example quantum machine setup is shown in the figure, below. The quantum device is a superconducting circuit composed of a single, fixed frequency qubit and a readout resonator, with the following Hamiltonian: .. math:: H = \frac{\hbar}{2}\omega_Q \sigma_z + \hbar \omega_R a^\dagger a + \hbar g (a^\dagger \sigma^- + a \sigma^+). Since the interaction between the qubit and resonator is dispersive (:math:`|\omega_R - \omega_Q| \gg g`), we can make an approximation that leads to the following form of the Hamiltonian: .. math:: H = \frac{\hbar}{2}\left( \omega_Q + \frac{g^2}{\Delta}\right)\sigma_z + \hbar \left( \omega_R + \frac{g^2}{\Delta}\sigma_z\right)a^\dagger a, where :math:`\Delta = \omega_Q - \omega_R`. Finally, we can explicitly include the qubit and resonator driving term, which leads to the Hamiltonian: .. math:: H = H_0 + \hbar s(t)\sigma_x + \frac{m(t)}{2}(a^\dagger e^{-i\omega t} + a e^{i\omega t}). We assume that the frequencies of both the qubit and the resonator were calibrated in advance. .. figure:: power_rabi1.png Setup of the Quantum Orchestration System Methodology =========== A signal, at the resonance frequency of the qubit, of the form .. math:: s(t) = A\cos(\omega_Q t + \phi) rotates the Bloch vector of the qubit at a rate A around the axis which is on the x-y plane and is rotated by an angle ϕ from the x-axis: .. figure:: methodology1.png :align: center Qubit rotation If the parameters A(t) and ϕ(t) are varied slowly compared to ωQ, then this still holds at each point in time. Thus, if a pulse is sent (i.e. a signal that is finite in time) to the qubit of the form .. math:: s(t) = A(t) \cos(\omega_Q t + \phi) where :math:`A(t)` varies slowly compared to :math:`\omega_Q`, the Bloch vector will be rotated around the above axis by a total angle which is given by the integral of :math:`A(t)`: .. math:: \theta = \int_{t_0}^{t_0 + \tau} A(t) dt Here :math:`t_0` is the time at which the pulse starts and :math:`\tau` is the duration of the pulse. In a typical Power Rabi Oscillations experiment, the shape and duration of the pulse :math:`A(t)` are fixed (e.g. a 20-nanosecond gaussian pulse) and only its amplitude is varied in order to get different rotation angles θ. The experiment performed by repeating the following basic sequence: 1. Initialize the qubit to the ground state, 0. 2. Apply a pulse with amplitude a (e.g. At is a gaussian shaped pulse with peak amplitude a), which rotates the qubit by :math:`\theta` so that the qubit is in the state .. math:: \cos(\theta_a)\left| 0\right> + \sin(\theta_a)e^{i\phi}\left| 1\right> 3. Apply a resonant pulse to the readout resonator, and from the phase of the reflected pulse, deduce the state of the qubit. This basic sequence is repeated in the program for a series of amplitudes (i.e., many values a), where for each amplitude, a, it is repeated N times (i.e. N identical basic sequences with the same a). N identical measurements are required because of state collapse. The measurement at the end of each basic sequence gives a binary result (0 or 1) for the state of the qubit, even if before the measurement the qubit was in a superposition state. However, when we average the results of the N identical basic sequences, the average will be :math:`\sin^2\theta`. We denote this average by :math:`P_{\left|1\right>(a)}` since it reflects the probability of measuring the qubit in the :math:`\left|1\right>` state for a given amplitude, a. The results of the whole experiment can be summarized by plotting as a function of a: .. figure:: oscillations1.png Power Rabi oscillations We can use this to calibrate any single qubit rotation gate that rotates the qubit by an angle θ, around a rotation axis that is on the x-y plane and is rotated ϕ from the x-axis. Such a gate is denoted by :math:`R_{\phi}(\theta)`. In fact, one of the typical goals of the Power Rabi Oscillations experiment is to calibrate the amplitude of a given pulse so that it performs π-rotation (X-gate) or π/2-rotation. ϕ, however, cannot be determined from the Rabi oscillations and must be determined by other means (e.g. tomography). Implementation ============== We now describe the implementation of the experiment from the software side, after having set up and characterized the system. As explained in section ‎2, the programming is done in Python using QM's package. However, it is crucial to keep in mind that in our case, Python is used as a "host" programming language in which we embed the QOP programming constructs. This is explained in more detail below. Steps ----- The experiment is implemented on the QOP as follows: 1. Defining a quantum machine configuration 2. Opening an interface to the quantum machine 3. Writing the program 4. Running the program 5. Saving the results Configuring the Quantum Machine ------------------------------- As discussed in section ‎2, the configuration is a description of the physical elements present in our experimental setup and their properties, as well as the connectivity between the elements and the OPXs. The physical elements that are connected to the OPXs are denoted in the configuration as elements, which are discrete entities such as qubits, readout resonators, flux lines, gate electrodes, etc. Each of these has inputs and in some cases outputs, connected to the OPXs. The properties of the elements and their connectivity to the OPXs are used by the QOP to interpret and execute QUA programs correctly (e.g. a pulse played to a certain qubit is modulated by the OPX with the intermediate frequency defined for this element). The pulses applied to the elements are also specified in the configuration, where each pulse is defined as a collection of temporal waveforms. For example, a pulse to an element with two analog inputs and one digital input will specify the two waveforms applied to the analog inputs of the element and the digital pulse applied to its digital input. Also in the configuration, specify the properties of auxiliary components that affect the actual output of the controller, such as IQ mixers and local oscillators. The configuration is specified as a set of nested Python dictionaries. Configuration for the Quantum Machine ------------------------------------- .. code-block:: python config = { 'version': 1, 'controllers': { 'con1': { 'type': 'opx1', 'analog_outputs': { 1: {'offset': +0.0}, 2: {'offset': +0.0}, 3: {'offset': +0.0}, 4: {'offset': +0.0}, }, 'digital_outputs': { 1: {}, }, 'analog_inputs': { 1: {'offset': +0.0}, } } }, 'elements': { 'qubit': { 'mixInputs': { 'I': ('con1', 1), 'Q': ('con1', 2), 'lo_frequency': 5.10e9, 'mixer': 'mixer_qubit' }, 'intermediate_frequency': 5.15e6, 'operations': { 'gauss_pulse': 'gauss_pulse_in' }, }, 'RR': { 'mixInputs': { 'I': ('con1', 3), 'Q': ('con1', 4), 'lo_frequency': 6.00e9, 'mixer': 'mixer_res' }, 'intermediate_frequency': 6.12e6, 'operations': { 'meas_pulse': 'meas_pulse_in', }, 'time_of_flight': 180, 'smearing': 0, 'outputs': { 'out1': ('con1', 1) } }, }, 'pulses': { 'meas_pulse_in': { 'operation': 'measurement', 'length': 20, 'waveforms': { 'I': 'gauss_wf', 'Q': 'zero_wf' }, 'integration_weights': { 'integW1': 'integW1', 'integW2': 'integW2', }, 'digital_marker': 'marker1' }, 'gauss_pulse_in': { 'operation': 'control', 'length': 20, 'waveforms': { 'I': 'exc_wf', 'Q': 'zero_wf' }, } }, 'waveforms': { 'exc_wf': { 'type': 'constant', 'sample': 0.479 }, 'zero_wf': { 'type': 'constant', 'sample': 0.0 }, 'gauss_wf': { 'type': 'arbitrary', 'samples': [0.005, 0.013, 0.02935, 0.05899883936462147, 0.10732436763802927, 0.1767030571463228, 0.2633180579359862, 0.35514694106994277, 0.43353720001453067, 0.479, 0.479, 0.4335372000145308, 0.3551469410699429, 0.26331805793598645, 0.17670305714632292, 0.10732436763802936, 0.05899883936462152, 0.029354822126316085, 0.01321923408389493, 0.005387955348880817] } }, 'digital_waveforms': { 'marker1': { 'samples': [(1, 4), (0, 2), (1, 1), (1, 0)] } }, 'integration_weights': { 'integW1': { 'cosine': [4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0], 'sine': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] }, 'integW2': { 'cosine': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'sine': [4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0] } }, 'mixers': { 'mixer_res': [ {'intermediate_frequency': 6.12e6, 'lo_freq': 6.00e9, 'correction': [1.0, 0.0, 0.0, 1.0]} ], 'mixer_qubit': [ {'intermediate_frequency': 5.15e6, 'lo_freq': 5.10e9, 'correction': [1.0, 0.0, 0.0, 1.0]} ], } } Because the programing interface is embedded in Python, we can use Python variables and functions when creating the configuration (as well as when writing QUA programs and when using the QM API). We could, for example define the samples of the gaus_wf waveform before the configuration is defined: .. code-block:: python gaus_pulse_len = 20 # nsec gaus_arg = np.linspace(-3, 3, gaus_pulse_len) gaus_wf = np.exp(-gaus_arg**2/2)gaus_wf = Amp * gaus_wf / np.max(gaus_wf) And then use this to define the samples of the waveform in the configuration: .. code-block:: python my_config = { 'version': 1, 'controllers': { 'con1': { 'type': 'opx1', 'analog': { . . . 'waveforms': { 'exc_wf': { 'type': 'constant', 'sample': 0.479 }, 'zero_wf': { 'type': 'constant', 'sample': 0.0 }, 'gauss_wf': { 'type': 'arbitrary', 'samples': gaus_wf.tolist() } } This approach can be used for all configuration elements. Opening the Quantum Machine --------------------------- After defining the configuration, we can open an interface to a new quantum machine with the following command: .. code-block:: python my_qm = qmManager.open_qm(my_config) Writing the Program ------------------- After having defined the configuration, write the QUA program. Here we show the power Rabi program, which is intuitive, while in the next section we describe in great detail the language, and list its commands: .. code-block:: python with program() as powerRabiProg : I = declare(fixed) Q = declare(fixed) a = declare(fixed) Nrep = declare(int) with for_(Nrep, 0, Nrep < 100, Nrep + 1): with for_(a, 0.00, a <= 1.0, a + 0.01): play('gauss_pulse'*amp(a), 'qubit') align("qubit", "RR") measure('meas_pulse', 'RR', 'samples',('integW1',I),('integW2',Q)) save(I, 'I') save(Q, 'Q') save(a, 'a') This program: - Defines the variables a (amplitude) and Nrep (number of repetitions), as well as the variables I and Q, which store the demodulation result. - Performs 100 repetitions (the loop over Nrep), where in each scan it: loops over 100 values of a, from 0 – 1 in increments of 0.01 and for each value of a performs the Rabi sequence: playing a pulse with amplitude a to the qubit, then measuring the resonator response and extracting from it the state of the qubit. This is done by sending a measurement pulse to the resonator and demodulating and integrating the returning pulse using the indicated integration weights. We also stream the raw data sampled at the OPX's input and save it with the label 'samples' and, and finally save the demodulation and integration results, I and Q, as well as the corresponding amplitude. This Python code block creates an object named ``powerRabiProg``, which is a QUA program that can be executed on an open quantum machine. It is important to note that this program, while embedded in Python, is not a Python program: it is a QUA program that will be compiled and run on the OPX controller in real-time with repeatable timing down to the single sample level. Running the Program ------------------- Run the program on my_qm: .. code-block:: python my_job = my_qm.execute(powerRabiProg) This command executes the powerRabiProg program and saves the results in the job object my_job. Pulling the Results ------------------- After the program is executed, the results can be pulled: .. code-block:: python time.sleep(1.0) my_powerRabi_results = job.get_results() Wait a short time (sleep) for the program to execute and then pull the results from ``my_job`` to the results object ``my_powerRabi_results``. The data in ``my_powerRabi_results`` is a Python object which contains the variables saved during the program, as well as all the raw data sampled at the input of the OPX. Here, ``my_powerRabi_results`` will have: - ``my_powerRabi_results.variable_results``, which will be a dictionary containing three keys: ``I``, ``Q`` and ``a``. The value for each key will be a dictionary containing the saved data and the time stamp for each saved data point. - ``my_powerRabi_results.raw_results``, which will be a dictionary containing a single key and its value will be a dictionary containing the sampled input data and the timestamp of each data point.