diff --git a/examples/bakery_examples/RB_1_qubit/RB_1qubit.py b/examples/bakery_examples/RB_1_qubit/RB_1qubit.py index 8b69ab9a..d437121d 100644 --- a/examples/bakery_examples/RB_1_qubit/RB_1qubit.py +++ b/examples/bakery_examples/RB_1_qubit/RB_1qubit.py @@ -45,10 +45,7 @@ assign(state, I > th) with if_(state): play("X", "qe1") - - play( - RB_baked_sequences[k].operations["qe1"], "qe1", truncate=truncate - ) # Truncate for RB seq of smaller lengths + RB_baked_sequences[k].run(trunc_array=[("qe1", truncate)]) RB_sequences[k].play_revert_op2(inverse_op) align("qe1", "rr") @@ -78,6 +75,8 @@ print("Inversion operations:", inv) print("Truncations indices:", truncate) +print(played_Cliffords) +print(played_inverse_Ops) # Plotting first baked RB sequence baked_pulse_I = config["waveforms"]["qe1_baked_wf_I_0"]["samples"] diff --git a/examples/bakery_examples/Ramsey_fringes/RamseyGauss_configuration.py b/examples/bakery_examples/Ramsey_fringes/RamseyGauss_configuration.py index b0a3e81d..f9969074 100644 --- a/examples/bakery_examples/Ramsey_fringes/RamseyGauss_configuration.py +++ b/examples/bakery_examples/Ramsey_fringes/RamseyGauss_configuration.py @@ -21,10 +21,10 @@ def IQ_imbalance_corr(g, phi): Drive_freq = 5e9 Tpihalf = 32 -Resonator_IF = 50e6 +Resonator_IF = 50e6 * 0 Resonator_LO = Resonator_freq - Resonator_IF -Drive_IF = 31.25e6 +Drive_IF = 31.25e6 * 0 Drive_LO = Drive_freq - Drive_IF Readout_Amp = 0.1 # meas pulse amplitude @@ -52,6 +52,16 @@ def IQ_imbalance_corr(g, phi): Input1_offset = 0.0 Input2_offset = 0.0 + +dephasing0 = 0 # phase at the origin of the 2nd Tpihalf gauss pulse +Tpihalf = 32 +wait_time_cc = 100 + +amplitude_pihalf = 1 +drive_cc = int(Tpihalf / 4) + 4 # 12cc = 48ns for Tpihalf=32 +if_freq = 31.25e6 +Fastload_length = 320 + config = { "version": 1, "controllers": { @@ -62,9 +72,7 @@ def IQ_imbalance_corr(g, phi): 2: {"offset": Resonator_Q0}, # Resonator Q 3: {"offset": Drive_I0}, # Drive I 4: {"offset": Drive_Q0}, # Drive Q - 5: { - "offset": 0 - }, # Drive LO amplitude modulation ---------------> SHOULD BE A DIGITAL OUTPUT + 5: {"offset": 0}, }, "digital_outputs": { 1: {}, # Resonator digital marker @@ -107,8 +115,8 @@ def IQ_imbalance_corr(g, phi): }, "Drive": { # Drive element "mixInputs": { - "I": ("con1", 4), - "Q": ("con1", 3), + "I": ("con1", 3), + "Q": ("con1", 4), "lo_frequency": Drive_LO, "mixer": "Drive_mixer", }, diff --git a/examples/bakery_examples/Ramsey_fringes/Ramsey_Gauss_baking.py b/examples/bakery_examples/Ramsey_fringes/Ramsey_Gauss_baking.py index 5b404ffb..4ade64aa 100644 --- a/examples/bakery_examples/Ramsey_fringes/Ramsey_Gauss_baking.py +++ b/examples/bakery_examples/Ramsey_fringes/Ramsey_Gauss_baking.py @@ -1,33 +1,115 @@ from qualang_tools.bakery import baking - +from qm.qua import * +from qm.QuantumMachinesManager import QuantumMachinesManager from RamseyGauss_configuration import * - - +from qm import SimulationConfig, LoopbackInterface from matplotlib import pyplot as plt -dephasingStep = 0 number_of_pulses = 32 +npts = 48 +dmax = int(npts / 4) baking_list = [] # Stores the baking objects -for i in range(number_of_pulses): # Create 16 different baked sequences +for i in range(number_of_pulses): # Create 32 different baked sequences with baking(config, padding_method="left") as b: init_delay = number_of_pulses # Put initial delay to ensure that all of the pulses will have the same length - - b.frame_rotation(dephasingStep, "Drive") b.wait( init_delay, "Drive" ) # This is to compensate for the extra delay the Resonator is experiencing. - # Play uploads the sample in the original config file (here we use an existing pulse in the config) b.play("gauss_drive", "Drive", amp=1) # duration Tpihalf+16 - b.play_at("gauss_drive", "Drive", init_delay - i) # duration Tpihalf + b.play_at("gauss_drive", "Drive", t=init_delay - i) # duration Tpihalf # Append the baking object in the list to call it from the QUA program baking_list.append(b) + + # You can retrieve and see the pulse you built for each baking object by modifying # index of the waveform plt.figure() for i in range(number_of_pulses): baked_pulse = config["waveforms"][f"Drive_baked_wf_I_{i}"]["samples"] - t = np.arange(0, len(baked_pulse), 1) - plt.plot(t, baked_pulse) + plt.plot(baked_pulse, label=f"pulse{i}") +plt.title("Baked Ramsey sequences") +plt.legend() + + +def play_ramsey(): + with switch_(j): + for k in range(len(baking_list)): + with case_(k): + baking_list[k].run() + + +with program() as RamseyGauss: # to measure Rabi flops every 1ns starting from 0ns + I = declare(fixed, value=0.0) + Q = declare(fixed) + I1 = declare(fixed) + Q1 = declare(fixed) + I2 = declare(fixed) + Q2 = declare(fixed) + + d = declare(int) + j = declare(int) + i_avg = declare(int) + + I_stream = declare_stream() + Q_stream = declare_stream() + + with for_(i_avg, 0, i_avg < 1000, i_avg + 1): + with for_(d, 0, d < dmax, d + 4): + with for_(j, 0, j < number_of_pulses, j + 1): + align( + "Drive", "Resonator" + ) # This align makes sure that the reset phase happens here. + wait( + 4 + 4 + d, "Drive" + ) # 11 - resonator reset phase, 4 - wait inside the switch-case, 4 - switch-case delay + play_ramsey() + wait(drive_cc + d + 0, "Resonator") + play("chargecav", "Resonator") # to charge the cavity + + measure( + "readout", + "Resonator", + None, + demod.full("integW_cos", I1, "out1"), + demod.full("integW_sin", Q1, "out1"), + demod.full("integW_cos", I2, "out2"), + demod.full("integW_sin", Q2, "out2"), + ) + assign( + I, I1 + Q2 + ) # summing over all the items in the vectors before assigning to the final I and Q variables + assign(Q, I2 - Q1) + save(I, I_stream) + save(Q, Q_stream) + + with stream_processing(): + I_stream.buffer(1000, npts).save("Iall") + Q_stream.buffer(1000, npts).save("Qall") + I_stream.buffer(1000, npts).save("IallNav") + Q_stream.buffer(1000, npts).save("QallNav") + +qmm = QuantumMachinesManager() +job = qmm.simulate( + config, + RamseyGauss, + SimulationConfig( + 16 * (wait_time_cc + Readout_pulse_length + Fastload_length + drive_cc) + ), +) +samps = job.get_simulated_samples() +plt.figure() +an1 = samps.con1.analog["1"].tolist() +an3 = samps.con1.analog["3"].tolist() +dig1 = samps.con1.digital["1"] +dig3 = samps.con1.digital["3"] + +plt.plot(an1) +plt.plot(an3) + +print("End prog") +plt.show() +print(job.result_handles.param2.fetch_all()) +print(job.result_handles.param3.fetch_all()) diff --git a/qualang_tools/bakery/bakery.py b/qualang_tools/bakery/bakery.py index 9010e27a..e6553b6b 100644 --- a/qualang_tools/bakery/bakery.py +++ b/qualang_tools/bakery/bakery.py @@ -3,7 +3,7 @@ Created: 23/02/2021 """ -from typing import Set, List, Union +from typing import List, Union import numpy as np from qm import qua import copy @@ -58,17 +58,20 @@ def _init_dict(self): sample_dict = {} qe_dict = {} for qe in self._config["elements"].keys(): + qe_dict[qe] = { + "time": 0, + "phase": 0.0, + "freq": 0, + "time_track": 0, + "phase_track": [0], + "freq_track": [0], + } if "mixInputs" in self._local_config["elements"][qe]: sample_dict[qe] = {"I": [], "Q": []} - qe_dict[qe] = { - "time": 0, - "phase": 0, - "time_track": 0, - "phase_track": [0], - } + elif "singleInput" in self._local_config["elements"][qe]: - sample_dict[qe] = [] - qe_dict[qe] = {"time": 0, "time_track": 0} + sample_dict[qe] = {"single": []} + return sample_dict, qe_dict def __exit__(self, exc_type, exc_value, exc_traceback): @@ -104,7 +107,7 @@ def __exit__(self, exc_type, exc_value, exc_traceback): if "mixInputs" in elements[qe]: end_samples = len(qe_samples["I"]) - wait_duration elif "singleInput" in elements[qe]: - end_samples = len(qe_samples) - wait_duration + end_samples = len(qe_samples["single"]) - wait_duration # Padding done according to desired method, can be either right, left, symmetric left or symmetric right if self._padding_method == "right": @@ -120,8 +123,9 @@ def __exit__(self, exc_type, exc_value, exc_traceback): + qe_samples["Q"][0:end_samples] ) elif "singleInput" in elements[qe]: - qe_samples = ( - qe_samples[end_samples:] + qe_samples[0:end_samples] + qe_samples["single"] = ( + qe_samples["single"][end_samples:] + + qe_samples["single"][0:end_samples] ) elif self._padding_method == "symmetric_l": @@ -130,14 +134,16 @@ def __exit__(self, exc_type, exc_value, exc_traceback): qe_samples["I"][end_samples + wait_duration // 2 :] + qe_samples["I"][0 : end_samples + wait_duration // 2] ) + qe_samples["Q"] = ( qe_samples["Q"][end_samples + wait_duration // 2 :] + qe_samples["Q"][0 : end_samples + wait_duration // 2] ) + elif "singleInput" in elements[qe]: - qe_samples = ( - qe_samples[end_samples + wait_duration // 2 :] - + qe_samples[0 : end_samples + wait_duration // 2] + qe_samples["single"] = ( + qe_samples["single"][end_samples + wait_duration // 2 :] + + qe_samples["single"][0 : end_samples + wait_duration // 2] ) elif self._padding_method == "symmetric_r": @@ -151,9 +157,11 @@ def __exit__(self, exc_type, exc_value, exc_traceback): + qe_samples["Q"][0 : end_samples + wait_duration // 2 + 1] ) elif "singleInput" in elements[qe]: - qe_samples = ( - qe_samples[end_samples + wait_duration // 2 + 1 :] - + qe_samples[0 : end_samples + wait_duration // 2 + 1] + qe_samples["single"] = ( + qe_samples["single"][end_samples + wait_duration // 2 + 1 :] + + qe_samples["single"][ + 0 : end_samples + wait_duration // 2 + 1 + ] ) # Generates new Op, pulse, and waveform for each qe to be added in the original config file @@ -179,15 +187,15 @@ def __exit__(self, exc_type, exc_value, exc_traceback): "samples": qe_samples["Q"], } - elif type(qe_samples) == list: + elif "single" in qe_samples: self._config["pulses"][f"{qe}_baked_pulse_{self._ctr}"] = { "operation": "control", - "length": len(qe_samples), + "length": len(qe_samples["single"]), "waveforms": {"single": f"{qe}_baked_wf_{self._ctr}"}, } self._config["waveforms"][f"{qe}_baked_wf_{self._ctr}"] = { "type": "arbitrary", - "samples": qe_samples, + "samples": qe_samples["single"], } def _get_samples(self, pulse: str) -> Union[List[float], List[List]]: @@ -234,7 +242,7 @@ def get_current_length(self, qe: str): if "mixInputs" in self._local_config["elements"][qe]: return len(self._samples_dict[qe]["I"]) elif "singleInput" in self._local_config["elements"][qe]: - return len(self._samples_dict[qe]) + return len(self._samples_dict[qe]["single"]) else: raise KeyError("quantum element not in the config") @@ -279,22 +287,38 @@ def get_Op_length(self, qe: str): self._config["waveforms"][f"{qe}_baked_wf_{self._ctr}"]["samples"] ) - def add_Op(self, name: str, qe: str, samples: list, digital_marker: str = None): + def add_Op( + self, + name: str, + qe: str, + samples: Union[list, list[list]], + digital_marker: str = None, + ): """ Adds in the configuration file a pulse element. :param name: name of the Operation to be added for the quantum element :param qe: targeted quantum element - :param samples: arbitrary waveform to be inserted into pulse definition + :param samples: arbitrary waveforms to be inserted into pulse definition :param digital_marker: name of the digital marker sample associated to the generated pulse (assumed to be in the original config) """ index = self._get_pulse_index(qe) Op = {name: f"{qe}_baked_pulse_b{self._ctr}_{index}"} + pulse = {} + waveform = {} if "mixInputs" in self._local_config["elements"][qe]: + if len(samples) != 2: + raise TypeError( + f"Number of provided samples not compatible with element {qe}" + ) + if len(samples[0]) != len(samples[1]): + raise IndexError( + "Error : samples provided for I and Q do not have the same length" + ) pulse = { f"{qe}_baked_pulse_b{self._ctr}_{index}": { "operation": "control", - "length": len(samples), + "length": len(samples[0]), "waveforms": { "I": f"{qe}_baked_b{self._ctr}_{index}_wf_I", "Q": f"{qe}_baked_b{self._ctr}_{index}_wf_Q", @@ -318,6 +342,10 @@ def add_Op(self, name: str, qe: str, samples: list, digital_marker: str = None): } elif "singleInput" in self._local_config["elements"][qe]: + if type(samples[0]) != float or type(samples[0]) != int: + raise TypeError( + f"Number of provided samples not compatible with element {qe}" + ) pulse = { f"{qe}_baked_pulse_b{self._ctr}_{index}": { "operation": "control", @@ -341,18 +369,20 @@ def add_Op(self, name: str, qe: str, samples: list, digital_marker: str = None): self._local_config["waveforms"].update(waveform) self._local_config["elements"][qe]["operations"].update(Op) - def play(self, Op: str, qe: str, amp: float = 1.0) -> None: + def play(self, Op: str, qe: str, amp: Union[float, tuple] = 1.0) -> None: """ Add a pulse to the baked sequence :param Op: operation to play to quantum element :param qe: targeted quantum element - :param amp: amplitude of the pulse (replaces amp(a)*'pulse' in QUA) + :param amp: amplitude of the pulse, can be either a float or a tuple of 4 variables (similar to amp(a) or amp(v00, v01, v10, v11) in QUA) :return: """ try: if self._qe_dict[qe]["time_track"] == 0: pulse = self._local_config["elements"][qe]["operations"][Op] samples = self._get_samples(pulse) + freq = self._qe_dict[qe]["freq"] + phi = self._qe_dict[qe]["phase"] if "mixInputs" in self._local_config["elements"][qe]: if (type(samples[0]) != list) or (type(samples[1]) != list): @@ -369,15 +399,33 @@ def play(self, Op: str, qe: str, amp: float = 1.0) -> None: Q = samples[1] I2 = [None] * len(I) Q2 = [None] * len(Q) - phi = self._qe_dict[qe]["phase"] + I3 = [None] * len(I) + Q3 = [None] * len(Q) for i in range(len(I)): - I2[i] = np.cos(phi) * I[i] - np.sin(phi) * Q[i] - Q2[i] = np.sin(phi) * I[i] + np.cos(phi) * Q[i] - self._samples_dict[qe]["I"].append(amp * I2[i]) - self._samples_dict[qe]["Q"].append(amp * Q2[i]) - self._qe_dict[qe]["phase_track"].append(phi) + if type(amp) == float or type(amp) == int: + I2[i] = amp * I[i] + Q2[i] = amp * Q[i] + elif len(amp) != 4 or type(amp) != tuple: + raise IndexError( + "Amplitudes provided must be stored in a tuple (v00, v01, v10, v11)" + ) + else: + I2[i] = amp[0] * I[i] + amp[1] * Q[i] + Q2[i] = amp[2] * I[i] + amp[3] * Q[i] + I3[i] = ( + np.cos(freq * i * 1e-9 + phi) * I2[i] + - np.sin(freq * i * 1e-9 + phi) * Q2[i] + ) + Q3[i] = ( + np.sin(freq * i * 1e-9 + phi) * I2[i] + + np.cos(freq * i * 1e-9 + phi) * Q2[i] + ) + self._samples_dict[qe]["I"].append(I3[i]) + self._samples_dict[qe]["Q"].append(Q3[i]) + self._qe_dict[qe]["phase_track"].append(phi) + self._qe_dict[qe]["freq_track"].append(freq) self._update_qe_time(qe, len(I)) elif "singleInput" in self._local_config["elements"][qe]: @@ -385,8 +433,12 @@ def play(self, Op: str, qe: str, amp: float = 1.0) -> None: raise TypeError( f"Error : samples given do not correspond to singleInput for element {qe} " ) - for sample in samples: - self._samples_dict[qe].append(amp * sample) + for i in range(len(samples)): + self._samples_dict[qe]["single"].append( + amp * np.cos(freq * i * 1e-9 + phi) * samples[i] + ) + self._qe_dict[qe]["phase_track"].append(phi) + self._qe_dict[qe]["freq_track"].append(freq) self._update_qe_time(qe, len(samples)) else: self.play_at(Op, qe, self._qe_dict[qe]["time_track"], amp) @@ -397,7 +449,7 @@ def play(self, Op: str, qe: str, amp: float = 1.0) -> None: f'Op:"{Op}" does not exist in configuration and not manually added (use add_pulse)' ) - def play_at(self, Op: str, qe: str, t: int, amp: float = 1.0) -> None: + def play_at(self, Op: str, qe: str, t: int, amp: Union[float, tuple] = 1.0) -> None: """ Add a waveform to the sequence at the specified time index. If indicated time is higher than the pulse duration for the specified quantum element, @@ -408,7 +460,7 @@ def play_at(self, Op: str, qe: str, t: int, amp: float = 1.0) -> None: :param Op: operation to play to quantum element :param qe: targeted quantum element :param t: Time tag in ns where the pulse should be added - :param amp: amplitude of the pulse (replaces amp(a)*'pulse' in QUA) + :param amp: amplitude of the pulse, can be either a float or a tuple of 4 variables (similar to amp(a) or amp(v00, v01, v10, v11) in QUA) :return: """ if type(t) != int: @@ -436,41 +488,81 @@ def play_at(self, Op: str, qe: str, t: int, amp: float = 1.0) -> None: raise IndexError( "Error : samples provided for I and Q do not have the same length" ) - I = samples[0] - Q = samples[1] - I2 = [None] * len(I) - Q2 = [None] * len(Q) + I, Q = samples[0], samples[1] + I2, Q2, I3, Q3 = ( + [None] * len(I), + [None] * len(I), + [None] * len(I), + [None] * len(I), + ) phi = self._qe_dict[qe]["phase_track"] - + freq = self._qe_dict[qe]["freq_track"] for i in range(len(I)): + if type(amp) == float or type(amp) == int: + I2[i] = amp * I[i] + Q2[i] = amp * Q[i] + else: + if len(amp) != 4 or type(amp) != tuple: + raise IndexError( + "Amplitudes provided must be stored in a tuple (v00, v01, v10, v11)" + ) + else: + I2[i] = amp[0] * I[i] + amp[1] * Q[i] + Q2[i] = amp[2] * I[i] + amp[3] * Q[i] if t + i < len(self._samples_dict[qe]["I"]): - I2[i] = ( - np.cos(phi[t + i]) * I[i] - np.sin(phi[t + i]) * Q[i] + I3[i] = ( + np.cos(freq[t + i] * (t + i) * 1e-9 + phi[t + i]) + * I2[i] + - np.sin(freq[t + i] * (t + i) * 1e-9 + phi[t + i]) + * Q2[i] ) - Q2[i] = ( - np.sin(phi[t + i]) * I[i] + np.cos(phi[t + i]) * Q[i] + Q3[i] = ( + np.sin(freq[t + i] * (t + i) * 1e-9 + phi[t + i]) + * I2[i] + + np.cos(freq[t + i] * (t + i) * 1e-9 + phi[t + i]) + * Q2[i] ) - self._samples_dict[qe]["I"][t + i] += amp * I2[i] - self._samples_dict[qe]["Q"][t + i] += amp * Q2[i] + + self._samples_dict[qe]["I"][t + i] += I3[i] + self._samples_dict[qe]["Q"][t + i] += Q3[i] else: phi = self._qe_dict[qe]["phase"] - I2[i] = np.cos(phi) * I[i] - np.sin(phi) * Q[i] - Q2[i] = np.sin(phi) * I[i] + np.cos(phi) * Q[i] - self._samples_dict[qe]["I"].append(amp * I2[i]) - self._samples_dict[qe]["Q"].append(amp * Q2[i]) + freq = self._qe_dict[qe]["freq"] + I3[i] = ( + np.cos(freq * i * 1e-9 + phi) * I2[i] + - np.sin(freq * i * 1e-9 + phi) * Q2[i] + ) + Q3[i] = ( + np.sin(freq * i * 1e-9 + phi) * I2[i] + + np.cos(freq * i * 1e-9 + phi) * Q2[i] + ) + + self._samples_dict[qe]["I"].append(I3[i]) + self._samples_dict[qe]["Q"].append(Q3[i]) self._qe_dict[qe]["phase_track"].append(phi) + self._qe_dict[qe]["freq_track"].append(freq) new_samples += 1 elif "singleInput" in self._local_config["elements"][qe]: + if type(amp) != float: + raise IndexError("Amplitude must be a number") if type(samples[0]) == list: raise TypeError( f"Error : samples given do not correspond to singleInput for element {qe} " ) for i in range(len(samples)): if t + i < len(self._samples_dict[qe]): - self._samples_dict[qe][t + i] += amp * samples[i] + phi = self._qe_dict[qe]["phase_track"][t + i] + freq = self._qe_dict[qe]["freq_track"][t + i] + self._samples_dict[qe]["single"][t + i] += ( + amp * np.cos(freq * (t + i) * 1e-9 + phi) * samples[i] + ) else: - self._samples_dict[qe].append(amp * samples[i]) + phi = self._qe_dict[qe]["phase"] + freq = self._qe_dict[qe]["freq"] + self._samples_dict[qe]["single"].append(amp * samples[i]) + self._qe_dict[qe]["phase_track"].append(phi) + self._qe_dict[qe]["freq_track"].append(freq) new_samples += 1 self._update_qe_time(qe, new_samples) @@ -483,16 +575,13 @@ def play_at(self, Op: str, qe: str, t: int, amp: float = 1.0) -> None: def frame_rotation(self, angle: float, qe: str): """ Shift the phase of the oscillator associated with a quantum element by the given angle. - This is typically used for virtual z-rotations. + This is typically used for virtual z-rotations. Frame rotation done within the baking sticks to the rest of the + QUA program after its execution. :param angle: phase parameter :param qe: quantum element """ - if "mixInputs" in self._local_config["elements"][qe]: - self._update_qe_phase(qe, angle) - else: - raise TypeError( - f"frame rotation not available for singleInput quantum element ({qe})" - ) + + self._update_qe_phase(qe, angle) def frame_rotation_2pi(self, angle: float, qe: str): """ @@ -501,25 +590,24 @@ def frame_rotation_2pi(self, angle: float, qe: str): :param angle: phase parameter :param qe: quantum element """ - if "mixInputs" in self._local_config["elements"][qe]: - self._update_qe_phase(qe, 2 * np.pi * angle) - else: - raise TypeError( - f"frame rotation not available for singleInput quantum element ({qe})" - ) - def reset_frame(self, *qe_set: Set[str]): + self._update_qe_phase(qe, 2 * np.pi * angle) + + def set_detuning(self, qe: str, freq: int): + """Update frequency by adding detuning to original IF set in the config. + Unlike frame rotation, the detuning will only affect the baked operation and will not stick in the element + :param qe quantum element + :param freq frequency of the detuning (in Hz) + """ + self._qe_dict[qe]["freq"] = freq + + def reset_frame(self, *qe_set: str): """ Used to reset all of the frame updated made up to this statement. :param qe_set: Set[str] of quantum elements """ for qe in qe_set: - if "mixInputs" in self._local_config["elements"][qe]: - self._update_qe_phase(qe, 0.0) - else: - raise TypeError( - f"reset frame not available for singleInput quantum element {qe}" - ) + self._update_qe_phase(qe, -self._qe_dict[qe]["phase"]) def ramp(self, amp: float, duration: int, qe: str): """ @@ -530,7 +618,7 @@ def ramp(self, amp: float, duration: int, qe: str): """ ramp_sample = [amp * t for t in range(duration)] if "singleInput" in self._local_config["elements"][qe]: - self._samples_dict[qe] += ramp_sample + self._samples_dict[qe]["single"] += ramp_sample elif "mixInputs" in self._local_config["elements"][qe]: self._samples_dict[qe]["Q"] += ramp_sample self._samples_dict[qe]["I"] += [0] * duration @@ -540,9 +628,9 @@ def _update_qe_time(self, qe: str, dt: int): self._qe_dict[qe]["time"] += dt def _update_qe_phase(self, qe: str, phi: float): - self._qe_dict[qe]["phase"] = phi + self._qe_dict[qe]["phase"] += phi - def wait(self, duration: int, *qe_set: Set[str]): + def wait(self, duration: int, *qe_set: str): """ Wait for the given duration on all provided elements. Here, the wait is simply adding 0 to the existing sample for a given duration. @@ -554,25 +642,27 @@ def wait(self, duration: int, *qe_set: Set[str]): if duration >= 0: for qe in qe_set: if qe in self._samples_dict.keys(): + + self._qe_dict[qe]["phase_track"] += [ + self._qe_dict[qe]["phase"] + ] * duration + self._qe_dict[qe]["freq_track"] += [ + self._qe_dict[qe]["freq"] + ] * duration if "mixInputs" in self._local_config["elements"][qe].keys(): - self._samples_dict[qe]["I"] = ( - self._samples_dict[qe]["I"] + [0] * duration - ) + self._samples_dict[qe]["I"] += [0] * duration self._samples_dict[qe]["Q"] += [0] * duration - self._qe_dict[qe]["phase_track"] += [ - self._qe_dict[qe]["phase"] - ] * duration elif "singleInput" in self._local_config["elements"][qe].keys(): - self._samples_dict[qe] += [0] * duration + self._samples_dict[qe]["single"] += [0] * duration self._update_qe_time(qe, duration) else: for qe in qe_set: - # Duration is negative so just add for substraction + # Duration is negative so just add for subtraction self._qe_dict[qe]["time_track"] = self._qe_dict[qe]["time"] + duration - def align(self, *qe_set: Set[str]): + def align(self, *qe_set: str): """ Align several quantum elements together. All of the quantum elements referenced in *elements will wait for all @@ -594,65 +684,79 @@ def align(self, *qe_set: Set[str]): if qe != last_qe: self.wait(last_t - qe_t, qe) - def run(self) -> None: + def run(self, amp_array=None, trunc_array=None) -> None: """ Plays the baked waveform - This method should be used within a QUA program + This method must be used within a QUA program + :param amp_array list of tuples for amplitudes (e.g [(qe1, amp1), (qe2, amp2)] ), each amplitude must be a scalar + :param trunc_array list of tuples for truncations (e.g [(qe1, amp1), (qe2, amp2)] ), each truncation must be a + int or QUA int :return None """ + qe_set = self.get_qe_set() + if len(qe_set) > 1: + qua.align(*qe_set) + if trunc_array is None: + if amp_array is None: + for qe in qe_set: + qua.play(f"baked_Op_{self._ctr}", qe) + qua.frame_rotation(self._qe_dict[qe]["phase"], qe) - if len(qe_set) == 1: - for qe in qe_set: - qua.play(f"baked_Op_{self._ctr}", qe) + else: + for qe in qe_set: + if not (qe in list(zip(*amp_array))[0]): + qua.play(f"baked_Op_{self._ctr}", qe) + + else: + index2 = list(zip(*amp_array))[0].index(qe) + amp = list(zip(*amp_array))[1][index2] + if amp == list: + raise TypeError( + "Amplitude can only be a number (either Python or QUA variable)" + ) + qua.play(f"baked_Op_{self._ctr}" * qua.amp(amp), qe) + qua.frame_rotation(self._qe_dict[qe]["phase"], qe) else: - qua.align(*qe_set) for qe in qe_set: - qua.play(f"baked_Op_{self._ctr}", qe) + if qe not in list(zip(*trunc_array))[0]: + trunc_array.append((qe, None)) + index = list(zip(*trunc_array))[0].index(qe) + trunc = list(zip(*trunc_array))[1][index] + if amp_array is None: + qua.play(f"baked_Op_{self._ctr}", qe, truncate=trunc) + else: + if not (qe in list(zip(*amp_array))[0]): + qua.play(f"baked_Op_{self._ctr}", qe, truncate=trunc) + + else: + index2 = list(zip(*amp_array))[0].index(qe) + amp = list(zip(*amp_array))[1][index2] + if amp == list: + raise TypeError( + "Amplitude can only be a number (either Python or QUA variable)" + ) + qua.play( + f"baked_Op_{self._ctr}" * qua.amp(amp), qe, truncate=trunc + ) + + qua.frame_rotation(self._qe_dict[qe]["phase"], qe) -def deterministic_run(baking_list): + +def deterministic_run(baking_list, j): """ Generates a QUA macro for a binary tree ensuring a synchronized play of operations listed in the various baking objects :param baking_list: Python list of Baking objects + :param j: QUA int """ - depth = int(np.ceil(np.log2(len(baking_list)))) - l = 0 - h = len(baking_list) - 1 - - def QUA_deterministic_tree(j, low: int = l, high: int = h, count: int = 1): - """ - QUA macro to be used in a QUA program - :param j: QUA int indicating which element of the baking list should be accessed - :param low: index indicating start of the list - :param high: index indicating the end of the list - :param count: counts the number of iterations, which should be the same for each accessed element - """ - - mid = (high + low) // 2 - - if count == depth: - if mid + 1 <= h: - with qua.if_(j > mid): - qua.wait(4, *baking_list[mid + 1].get_qe_set()) - baking_list[mid + 1].run() - with qua.else_(): - qua.wait(4, *baking_list[mid].get_qe_set()) - baking_list[mid].run() - else: - baking_list[mid].run() - - else: - - with qua.if_(j > mid): - QUA_deterministic_tree(j, mid + 1, high, count + 1) - - with qua.else_(): - QUA_deterministic_tree(j, low, mid, count + 1) - return QUA_deterministic_tree + with qua.switch_(j): + for i in range(len(baking_list)): + with qua.case_(i): + baking_list[i].run() class BakingOperations: diff --git a/qualang_tools/bakery/randomized_benchmark.py b/qualang_tools/bakery/randomized_benchmark.py index 1b1f2506..64aba346 100644 --- a/qualang_tools/bakery/randomized_benchmark.py +++ b/qualang_tools/bakery/randomized_benchmark.py @@ -36,7 +36,6 @@ csv = pkg_resources.open_text(bakery_resources, "c1_cayley_table.csv") - c1_table = pd.read_csv(csv).to_numpy()[:, 1:] @@ -67,6 +66,17 @@ def find_revert_op(input_state_index: int): return i +def play_revert_op(index: int, baked_cliffords): + """Plays an operation resetting qubit in its ground state based on the + transformation provided by the index in Cayley table (switch using baked Cliffords) + :param index index of the transformed qubit state""" + + with switch_(index): + for i in range(len(baked_cliffords)): + with case_(i): + baked_cliffords[i].run() + + class RBSequence: def __init__(self, config: dict, d_max: int, qubit: str): self.d_max = d_max @@ -85,16 +95,6 @@ def __init__(self, config: dict, d_max: int, qubit: str): self.inverse_op_string = [""] * d_max self.sequence = self.generate_RB_sequence() # Store the RB sequence - def play_revert_op(self, index: int): - """Plays an operation resetting qubit in its ground state based on the - transformation provided by the index in Cayley table (switch using baked Cliffords) - :param index index of the transformed qubit state""" - - with switch_(index): - for i in range(len(self.baked_cliffords)): - with case_(i): - self.baked_cliffords[i].run() - def play_revert_op2(self, index: int): """Plays an operation resetting qubit in its ground state based on the transformation provided by the index in Cayley table (explicit switch case) diff --git a/qualang_tools/bakery/readme.md b/qualang_tools/bakery/readme.md new file mode 100644 index 00000000..20200183 --- /dev/null +++ b/qualang_tools/bakery/readme.md @@ -0,0 +1,246 @@ +# Waveform baking +This library introduces a new framework for creating arbitrary waveforms and storing them in the usual configuration file. +The idea is to simplify the writing of pulse samples without the limitation imposed by the hardware. Using this tool provides an advantage by embedding into one single waveform a series of instructions that allows program memory preservation. +Waveform baking is done via a new context manager, declared prior to the QUA program, that takes two inputs: + +- the configuration dictionary (the same used to initialize a Quantum Machine instance), + +- a padding method: to be chosen between: “right”, “left”, “symmetric_l”, “symmetric_r”. This string indicates how samples should be filled up with 0s when they do not match hardware constraints (that is if waveform's length is not a multiple of 4 or is shorter than 16 ns). + + - “right” setting is the default setting and pads 0s at the end of the baked samples + + - “left” pads 0s before the baked samples + + - “symmetric_l” pads 0s symmetrically before and after the baked samples, putting one more 0 before it in case the baked waveform's length is odd + + - “symmetric_r' pads 0s symmetrically before and after the baked samples , putting one more 0 after it in case the baked waveform's length is odd + +Declaration is done before the QUA program as follows: + +``` +with baking(config, padding_method = "symmetric_r") as b: + b.align("qe1", "qe2", "qe3") + b.frame_rotation(0.78, "qe2") + b.ramp(amp=0.3, duration=9, qe="qe1") +``` + +When executed, the content manager edits the input configuration file and adds: +- an operation for each quantum element involved within the baking context manager +- an associated pulse +- an associated waveform (set of 2 waveforms for a mixedInputs quantum element) containing waveform(s) issued from concatenation of operations indicated in the context manager. + + +# **How can I add operations inside the baking context manager?** + +The logic behind the baking context manager is to stay as close as possible to the way we would write play statements within a QUA program. For instance, commands like frame_rotation, reset_phase, ramp, wait and align are all replicated within the context manager. + +The procedure for using baked operations is as follows: + +1. You first have to write down the samples you want to use as a waveform in the form of a Python list. + - If the samples is meant for a singleInput element, the list should contain the samples itself. + - Contrariwise, if it is intended for a mixInputs element, the list should contain two Python lists as ```[sample_I, sample_Q]``` , where sample_I and sample_Q are themselves Python lists containing the samples. + +2. Add the samples to the local configuration, with method ```add_Op```, which takes 4 inputs: + - The name of the operation (name you will use only within the baking context manager in a play statement) + - The quantum element for which you want to add the operation + - The samples to store as waveforms + - The digital_marker name (supposedly already existing in the configuration) to attach to the pulse associated to the operation. + +3. Use a baking ``play()`` statement, specifying the operation name (which should correspond to the name introduced in the add_Op method) and the quantum element to play the operation on + +All those commands concatenated altogether eventually build one single “big” waveform per quantum element involved in the baking that contains all the instructions specified in the baking environment. The exiting procedure of the baking ensures that the appropriate padding is done to ensure that the OPX will be able to play this arbitrary waveform. + +Here is a basic code example that simply plays two pulses of short lengths: + + +``` +with baking(config, padding_method = "symmetric_r") as b: + +# Create arbitrary waveforms + + singleInput_sample = [1., 0.8, 0.6, 0.8, 0.9] + mixInput_sample_I = [1.2, 0.3, 0.5] + mixInput_sample_Q = [0.8, 0.2, 0.4] + + # Assign waveforms to quantum element operation + + b.add_Op("single_Input_Op", "qe1", singleInput_sample, digital_marker= None) + b.add_Op("mix_Input_Op", "qe2", [mixInput_sample_I, mixInput_sample_Q], digital_marker = None) + + # Play the operations + + b.play("single_Input_Op", "qe1") + b.play("mix_Input_Op", "qe2") +``` +# **How to play in QUA my baked waveforms?** + +The baking object has a method called run, which takes no inputs and simply does appropriate alignment between quantum elements involved in the baking and play simultaneously (using this time a QUA play statement) the previously baked waveforms. Therefore, all that is left is to **call the run method associated to the baking object within the actual QUA program**. + +``` +with baking(config, "left"): + #Create your baked waveform, see snippet above + +#Open QUA program: +with program() as QUA_prog: + b.run() +``` + +As in QUA, you can still modulate in real time (using QUA variables) properties of the pulse like its amplitude, or to truncate it. +You can indeed pass into a set of two lists, parameters for truncating pulses and for amplitude modulation. The syntax goes as follows: +``` +with program() as QUA_prog: + truncate = declare(int, value = 18) + amp = declare(fixed, value = 0.4) + b.run(amp_array = [(qe1, amp), (qe2, 0.5)], truncate_array = [(qe1, truncate), (qe3, 74)]) +``` +Note that you do not have to provide tuples for every quantum element. The parameters you can pass can either Python or QUA variables. Beware though, you should make sure that the elements +indicated in the parameter arrays are actually used within the baking context manager. +# **Additional features of the baking environment** + +The baking aims to be as versatile as possible in the way of editing samples. The idea is therefore to generate desired samples up to the precision of the nanosecond, without having to worry about its format and its insertion in the configuration file. It is even possible to generate a waveform based on two previous samples (like a pulse superposition) by using two commands introduced in the baking: ``play_at()`` and ``negative_wait()``. + +Let’s take a look at the code below to understand what these two features do: + +``` +with baking(config=config, padding_method="symmetric_r") as b: + const_Op = [0.3, 0.3, 0.3, 0.3, 0.3] + const_Op2 = [0.2, 0.2, 0.2, 0.3, 0.4] + b.add_Op("Op1", "qe1", [const_Op, const_Op2]) # qe1 is a mixInputs element + Op3 = [1., 1., 1.] + Op4 = [2., 2., 2.] + b.add_Op("Op2", "qe1", [Op3, Op4]) + b.play("Op1", "qe1") + + # The baked waveform is at this point I: [0.3, 0.3, 0.3, 0.3, 0.3] + # Q: [0.2, 0.2, 0.2, 0.3, 0.4] + + b.play_at("Op3", "qe1", t=2) + # t indicates the time index where these new samples should be added + # The baked waveform is now I: [0.3, 0.3, 1.3, 1.3, 1.3] + # Q: [0.2, 0.2, 2.2, 2.3, 2.4] + +At the baking exit, the config will have an updated samples +adapted for QUA compilation, according to the padding_method chosen, in this case: +I: [0, 0, 0, 0, 0, 0.3, 0.3, 1.3, 1.3, 1.3, 0, 0, 0, 0, 0, 0], +Q: [0, 0, 0, 0, 0, 0.2, 0.2, 2.2, 2.3, 2.4, 0, 0, 0, 0, 0, 0] +``` +If the time index t is positive, the samples will be added precisely at the index indicated in the existing samples. +Contrariwise, if the provided index t is negative, we call here automatically the function ``negative_wait()``, which adds the samples at the provided index starting to count from the end of the existing samples: +``` +with baking(config=config, padding_method="symmetric_r") as b: + const_Op = [0.3, 0.3, 0.3, 0.3, 0.3] + const_Op2 = [0.2, 0.2, 0.2, 0.3, 0.4] + b.add_Op("Op1", "qe1", [const_Op, const_Op2]) #qe1 is a mixInputs element + Op3 = [1., 1., 1.] + Op4 = [2., 2., 2.] + b.add_Op("Op2", "qe1", [Op3, Op4]) + b.play("Op1", "qe1") + # The baked waveform is at this point I: [0.3, 0.3, 0.3, 0.3, 0.3] + # Q: [0.2, 0.2, 0.2, 0.3, 0.4] + b.play_at("Op3", "qe1", t=-2) #t indicates the time index where these new samples should be added + # The baked waveform is now I: [0.3, 0.3, 0.3, 1.3, 1.3, 1.0] + # Q: [0.2, 0.2, 0.2, 2.3, 2.4, 2.0] + +At the baking exit, the config will have updated samples +adapted for QUA compilation, according to the padding_method chosen, in this case: """ +I: [0, 0, 0, 0, 0, 0.3, 0.3, 0.3, 1.3, 1.3, 1.0, 0, 0, 0, 0, 0], +Q: [0, 0, 0, 0, 0, 0.2, 0.2, 0.2, 2.3, 2.4, 2.0, 0, 0, 0, 0, 0] +``` +The ``play_at()`` command can also be used as a single play statement involving a wait time and a play statement. In fact, if the time index indicated in the function is actually out of the range of the existing samples, a wait command is automatically added until reaching this time index (recall that the index corresponds to the time in ns) and starts inserting the operation indicated at this time. See the example below: + +``` +with baking(config=config, padding_method="symmetric_r") as b: + const_Op = [0.3, 0.3, 0.3, 0.3, 0.3] + const_Op2 = [0.2, 0.2, 0.2, 0.3, 0.4] + b.add_Op("Op1", "qe1", [const_Op, const_Op2]) #qe1 is a mixInputs element + Op3 = [1., 1., 1.] + Op4 = [2., 2., 2.] + b.add_Op("Op2", "qe1", [Op3, Op4]) + b.play("Op1", "qe1") + # The baked waveform is at this point I: [0.3, 0.3, 0.3, 0.3, 0.3] + # Q: [0.2, 0.2, 0.2, 0.3, 0.4] + b.play_at("Op3", "qe1", t=8) #t indicates the time index where these new samples should be added + # The baked waveform is now + # I: [0.3, 0.3, 0.3, 0.3, 0.3, 0, 0, 0, 1.0, 1.0, 1.0], + # Q: [0.2, 0.2, 0.2, 0.3, 0.4, 0, 0, 0, 2.0, 2.0, 2.0]} + # + +At the baking exit, the config will have updated samples +adapted for QUA compilation, according to the padding_method chosen, in this case: +I: [0.3, 0.3, 0.3, 0.3, 0.3, 0, 0, 0, 1.0, 1.0, 1.0, 0, 0, 0, 0, 0], +Q: [0.2, 0.2, 0.2, 0.3, 0.4, 0, 0, 0, 2.0, 2.0, 2.0, 0, 0, 0, 0, 0] +``` + +# The negative wait + +Negative wait is at the moment, just an equivalent way of writing the ``play_at()`` statement. + +The idea is to move backwards the time index at which the following play statement should start (wait[-3] means that the following waveform will be added on top of the existing sequence on the 3 last samples and will append the rest like a usual play statement. + +We have the equivalence between: +``` +b.wait(-3) +b.play('my_pulse',qe) +``` +and +``` +b.play_at('my_pulse', qe, t=-3) +``` + +# **Examples** + +## Ramsey at short time scales +[Ramsey](Ramsey_Gauss_baking.py) - In this baking example, we are creating pulses for a Ramsey experiment in which the +$$\pi/2$$ pulses are made using gaussian with a width of 5 ns, and the distance between the two pulses changes from 0 to +32 ns. It is also possible to change the phase of the second pulse using the *dephasingStep* parameter. The resulting +pulses are plotted, it is important to remember that these pulses are in the baseband, and will be multiplied by the IF +matrix (and later mixed with an LO). + +### Generating short Ramsey pulse sequences with waveform baking + +This tutorial presents a use case for the waveform baking tool, which facilitates the +generation of pulse samples that are shorter than 16 ns, which would usually have to be manually +modified to upload it to the OPX. + +Using the baking environment before launching the QUA program allows the pulse to be seamlessly +integrated in the configuration file, without having to account for the restrictions of pulse length +imposed by QUA. + +It also provides a simpler interface to generate one single waveform that can contain several +play statements (preserving program memory). + +The experiment is as follows: + +We have a superconducting qubit (controlled using the quantum element 'Drive' in the configuration +file) coupled to a readout resonator ('Resonator') with which we would like to apply sequences +of two short Gaussian pulses spaced with a varying time duration, followed directly by a probe +coming from the resonator (the measurement procedure should start immediately after the second Gaussian +pulse was played by the Drive element). + +The baking environment is used here to synthesize without any effort a waveform resulting from delayed +superposition of two Gaussian pulses (a simple *play* followed by a *play_at* with a varying delay). +Note that we also use an initial delay to ensure that there is a perfect +synchronization between the end of the Ramsey sequence, and the trigger of the resonator +for probing the qubit state. + +Within the QUA program, what remains to do is simply launching the created baking objects within +a Python for loop (using the *run* command) and use all appropriate commands related to the resonator to build your experiment. + + + +# Randomized benchmarking for 1 qubit with waveform baking + +Waveform baking is a tool to be used prior to running a QUA program to store waveforms and play them +easily within the QUA program without having to require a series of play statements. + +It turns out that this economy of statements can be particularly useful for saving program +memory when running long characterization experiments that do require lots of pulses to be played, +such as tomography experiments (usually involving state preparation, process, and readout for each couple of input state +and readout observable to be sampled) or randomized benchmarking. +Randomized benchmarking principles are reminded in another example done in QUA: https://docs.qualang.io/libs/examples/randomized-benchmark/one-qubit-rb/ +Here, the idea is to show the ease with which we can integrate tools associated to waveform baking to the example realized in the existing QUA script , +by taking the same elementary built-in functions to generate the entire quantum circuit necessary to run the random sequence. +With the use of the baking, we now have one single baked waveform randomly +synthesized. + +