From 4d0f0134cf4353fc3575516e37ea7b2835396e28 Mon Sep 17 00:00:00 2001 From: Arthur Strauss <56998701+arthurostrauss@users.noreply.github.com> Date: Sun, 1 Aug 2021 09:35:38 +0200 Subject: [PATCH] Baking compatibility with add_compiled (#12) * Update of the config now optional+ possibility to retrieve baked wfs Baking now allows a simple retrieval of baked waveforms, that can be created according under the constraint of being of a certain length (to ensure compatibility with overriding waveforms with add_compiled feature). Update of original config file no more compulsory, one can now also specify if update of the config should induce an overridable waveform or not * Added example for pre_compile usage + changed the input parameters for b Instead of declaring a matching length constraint, one has now to indicate a baking index that indicates which baked waveform should be overwritten (and ensure it matches its length for overriding it with add_compiled) * Reformat * Added method delete_baked_Op This new method (called using b.delete_baked_Op(qe)) allows the direct removal from the input config of the baking object the associated baked operation and associated waveforms --- .../baked_add_compile.py | 45 +++++ .../pre_compile_compatibility/config.py | 170 ++++++++++++++++ qualang_tools/bakery/bakery.py | 187 +++++++++++++----- 3 files changed, 353 insertions(+), 49 deletions(-) create mode 100644 examples/bakery_examples/pre_compile_compatibility/baked_add_compile.py create mode 100644 examples/bakery_examples/pre_compile_compatibility/config.py diff --git a/examples/bakery_examples/pre_compile_compatibility/baked_add_compile.py b/examples/bakery_examples/pre_compile_compatibility/baked_add_compile.py new file mode 100644 index 00000000..4d215f14 --- /dev/null +++ b/examples/bakery_examples/pre_compile_compatibility/baked_add_compile.py @@ -0,0 +1,45 @@ +from qualang_tools import baking +from qm.qua import * +from qm.QuantumMachinesManager import QuantumMachinesManager +from config import config + +# Create a first baked waveform, overridable +# and which will be inserted in the config +with baking( + config, padding_method="right", override=True, update_config=True +) as b_template: + samples_I = [0.1, 0.1, 0.2, 0.1, 0.2] + samples_Q = [0.2, 0.2, 0.3, 0.1, 0.0] + b_template.add_Op("Op", "qe1", [samples_I, samples_Q]) + b_template.play("Op", "qe1") + +# Re-open the context manager with either same baking object (b_template) or a new one (b_new) to generate a +# new waveform +# Only important thing is to indicate which baking index it shall use to generate the right name to override waveform +# Note that override parameter and update_config are not relevant anymore (since program is already compiled with a +# previous config, as we only want to retrieve the waveforms out +# of this new baking object +with baking( + config, + padding_method="right", + override=False, + update_config=False, + baking_index=b_template.get_baking_index(), +) as b_new: + samples_I = [0.3, 0.3, 0.4] + samples_Q = [0.0, 0.1, 0.2] + b_new.add_Op("Op", "qe1", [samples_I, samples_Q]) + b_new.play("Op", "qe1") + +print(b_template.get_waveforms_dict()) +print(b_new.get_waveforms_dict()) +qmm = QuantumMachinesManager() +qm = qmm.open_qm(config) + +with program() as prog: + b_template.run() + +pid = qm.queue.compile(prog) +pjob = qm.queue.add_compiled(prog, overrides={b_new.get_waveforms_dict()}) +job = qm.queue.wait_for_execution(pjob) +job.results_handles.wait_for_all_values() diff --git a/examples/bakery_examples/pre_compile_compatibility/config.py b/examples/bakery_examples/pre_compile_compatibility/config.py new file mode 100644 index 00000000..e0d72ebc --- /dev/null +++ b/examples/bakery_examples/pre_compile_compatibility/config.py @@ -0,0 +1,170 @@ +import numpy as np + +pulse_len = 80 +readout_len = 400 +qubit_IF = 50e6 +rr_IF = 50e6 +qubit_LO = 6.345e9 +rr_LO = 4.755e9 + + +def gauss(amplitude, mu, sigma, length): + t = np.linspace(-length / 2, length / 2, length) + gauss_wave = amplitude * np.exp(-((t - mu) ** 2) / (2 * sigma ** 2)) + return [float(x) for x in gauss_wave] + + +def IQ_imbalance(g, phi): + c = np.cos(phi) + s = np.sin(phi) + N = 1 / ((1 - g ** 2) * (2 * c ** 2 - 1)) + return [float(N * x) for x in [(1 - g) * c, (1 + g) * s, (1 - g) * s, (1 + g) * c]] + + +gauss_pulse = gauss(0.2, 0, 20, pulse_len) + +config = { + "version": 1, + "controllers": { + "con1": { + "type": "opx1", + "analog_outputs": { + 1: {"offset": +0.0}, # qe-I + 2: {"offset": +0.0}, # qe-Q + 3: {"offset": +0.0}, # rr-I + 4: {"offset": +0.0}, # rr-Q + }, + "digital_outputs": { + 1: {}, + }, + "analog_inputs": { + 1: {"offset": +0.0}, + }, + } + }, + "elements": { + "qe1": { + "mixInputs": { + "I": ("con1", 1), + "Q": ("con1", 2), + "lo_frequency": qubit_LO, + "mixer": "mixer_qubit", + }, + "intermediate_frequency": qubit_IF, + "operations": { + "I": "IPulse", + "X/2": "X/2Pulse", + "X": "XPulse", + "-X/2": "-X/2Pulse", + "Y/2": "Y/2Pulse", + "Y": "YPulse", + "-Y/2": "-Y/2Pulse", + }, + }, + "rr": { + "mixInputs": { + "I": ("con1", 3), + "Q": ("con1", 4), + "lo_frequency": rr_LO, + "mixer": "mixer_RR", + }, + "intermediate_frequency": rr_IF, + "operations": { + "readout": "readout_pulse", + }, + "outputs": {"out1": ("con1", 1)}, + "time_of_flight": 28, + "smearing": 0, + }, + }, + "pulses": { + "constPulse": { + "operation": "control", + "length": pulse_len, + "waveforms": {"I": "gauss_wf", "Q": "gauss_wf"}, + }, + "IPulse": { + "operation": "control", + "length": pulse_len, + "waveforms": {"I": "zero_wf", "Q": "zero_wf"}, + }, + "XPulse": { + "operation": "control", + "length": pulse_len, + "waveforms": {"I": "const_wf", "Q": "zero_wf"}, + }, + "X/2Pulse": { + "operation": "control", + "length": pulse_len, + "waveforms": {"I": "pi/2_wf", "Q": "zero_wf"}, + }, + "-X/2Pulse": { + "operation": "control", + "length": pulse_len, + "waveforms": {"I": "-pi/2_wf", "Q": "zero_wf"}, + }, + "YPulse": { + "operation": "control", + "length": pulse_len, + "waveforms": {"I": "zero_wf", "Q": "pi_wf"}, + }, + "Y/2Pulse": { + "operation": "control", + "length": pulse_len, + "waveforms": {"I": "zero_wf", "Q": "pi/2_wf"}, + }, + "-Y/2Pulse": { + "operation": "control", + "length": pulse_len, + "waveforms": {"I": "zero_wf", "Q": "-pi/2_wf"}, + }, + "readout_pulse": { + "operation": "measurement", + "length": readout_len, + "waveforms": {"I": "readout_wf", "Q": "zero_wf"}, + "integration_weights": { + "integW1": "integW1", + "integW2": "integW2", + }, + "digital_marker": "ON", + }, + }, + "waveforms": { + "const_wf": {"type": "constant", "sample": 0.2}, + "gauss_wf": {"type": "arbitrary", "samples": gauss_pulse}, + "pi_wf": {"type": "arbitrary", "samples": gauss(0.2, 0, 12, pulse_len)}, + "-pi/2_wf": {"type": "arbitrary", "samples": gauss(-0.1, 0, 12, pulse_len)}, + "pi/2_wf": {"type": "arbitrary", "samples": gauss(0.1, 0, 12, pulse_len)}, + "zero_wf": {"type": "constant", "sample": 0.0}, + "readout_wf": {"type": "constant", "sample": 0.3}, + }, + "digital_waveforms": { + "ON": {"samples": [(1, 0)]}, + }, + "integration_weights": { + "integW1": { + "cosine": [1.0] * int(readout_len / 4), + "sine": [0.0] * int(readout_len / 4), + }, + "integW2": { + "cosine": [0.0] * int(readout_len / 4), + "sine": [1.0] * int(readout_len / 4), + }, + }, + "mixers": { + "mixer_qubit": [ + { + "intermediate_frequency": qubit_IF, + "lo_frequency": qubit_LO, + "correction": IQ_imbalance(0.0, 0.0), + } + ], + "mixer_RR": [ + { + "intermediate_frequency": rr_IF, + "lo_frequency": rr_LO, + "correction": IQ_imbalance(0.0, 0.0), + } + ], + }, +} diff --git a/qualang_tools/bakery/bakery.py b/qualang_tools/bakery/bakery.py index e6553b6b..d54832fe 100644 --- a/qualang_tools/bakery/bakery.py +++ b/qualang_tools/bakery/bakery.py @@ -9,20 +9,50 @@ import copy -def baking(config, padding_method="right"): - return Baking(config, padding_method) +def baking( + config, + padding_method="right", + override=False, + update_config: bool = True, + baking_index: int = None, +): + """ + Opens a context manager to synthesize samples for arbitrary waveforms + :param config: config file + :param padding_method: Method to pad 0s to format the waveform to match hardware constraint + (>16 ns and multiple of 4) + :param override: Define if baked waveforms are overridable when using add_compiled feature + :param update_config: Define if baked waveform should be added within the input config file + :param baking_index: index of a reference baking object to impose length constraint on new baked waveform + (useful for matching lengths when using waveform overriding in add_compiled feature) + :return: No return, config is updated if update_config is set to True, + generated waveforms can be retrieved using the get_waveform_dict() method, in a format readily pluggable as an overrides argument + """ + return Baking(config, padding_method, override, update_config, baking_index) class Baking: - def __init__(self, config, padding_method="right"): + def __init__( + self, + config, + padding_method: str = "right", + override: bool = False, + update_config: bool = True, + baking_index: int = None, + ): self._config = config + self.update_config = update_config self._padding_method = padding_method self._local_config = copy.deepcopy(config) self._samples_dict, self._qe_dict = self._init_dict() - self._ctr = self._get_baking_index() # unique name counter + self._ctr = self._find_baking_index(baking_index) # unique name counter self._qe_set = set() + self.override = override + self.length_constraint = self._retrieve_constraint_length(baking_index) + self.override_waveforms_dict = {"waveforms": {}} def __enter__(self): + self._samples_dict, self._qe_dict = self._init_dict() return self @property @@ -43,16 +73,18 @@ def operations(self): def config(self): return self._config - def _get_baking_index(self): - index = 0 - max_index = [-1] - for qe in self._config["elements"].keys(): - index = [-1] - for op in self._config["elements"][qe]["operations"]: - if op.find("baked") != -1: - index.append(int(op.split("_")[-1])) - max_index.append(max(index)) - return max(max_index) + 1 + def _find_baking_index(self, baking_index: int = None): + if self.update_config and baking_index is None: + max_index = [-1] + for qe in self._config["elements"].keys(): + index = [-1] + for op in self._config["elements"][qe]["operations"]: + if op.find("baked") != -1: + index.append(int(op.split("_")[-1])) + max_index.append(max(index)) + return max(max_index) + 1 + else: + return baking_index def _init_dict(self): sample_dict = {} @@ -74,6 +106,45 @@ def _init_dict(self): return sample_dict, qe_dict + def _update_config(self, qe, qe_samples): + + # Generates new Op, pulse, and waveform for each qe to be added in the original config file + + self._config["elements"][qe]["operations"][ + f"baked_Op_{self._ctr}" + ] = f"{qe}_baked_pulse_{self._ctr}" + if "I" in qe_samples: + self._config["pulses"][f"{qe}_baked_pulse_{self._ctr}"] = { + "operation": "control", + "length": len(qe_samples["I"]), + "waveforms": { + "I": f"{qe}_baked_wf_I_{self._ctr}", + "Q": f"{qe}_baked_wf_Q_{self._ctr}", + }, + } + self._config["waveforms"][f"{qe}_baked_wf_I_{self._ctr}"] = { + "type": "arbitrary", + "samples": qe_samples["I"], + "is_overridable": self.override, + } + self._config["waveforms"][f"{qe}_baked_wf_Q_{self._ctr}"] = { + "type": "arbitrary", + "samples": qe_samples["Q"], + "is_overridable": self.override, + } + + elif "single" in qe_samples: + self._config["pulses"][f"{qe}_baked_pulse_{self._ctr}"] = { + "operation": "control", + "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["single"], + "is_overridable": self.override, + } + def __exit__(self, exc_type, exc_value, exc_traceback): """ Updates the configuration dictionary upon exit @@ -91,13 +162,20 @@ def __exit__(self, exc_type, exc_value, exc_traceback): ): # Check if a sample was added to the quantum element # otherwise we do not add any Op self._qe_set.add(qe) + if self.length_constraint is not None: + assert self._qe_dict[qe]["time"] < self.length_constraint, ( + f"Provided length constraint (={self.length_constraint}) " + f"smaller than actual baked samples length ({self._qe_dict[qe]['time']})" + ) + wait_duration += self.length_constraint - self._qe_dict[qe]["time"] + self.wait(self.length_constraint - self._qe_dict[qe]["time"]) if ( self._qe_dict[qe]["time"] < 16 ): # Sample length must be at least 16 ns long wait_duration += 16 - self._qe_dict[qe]["time"] self.wait(16 - self._qe_dict[qe]["time"], qe) - if not ( - self._qe_dict[qe]["time"] % 4 == 0 + if ( + not self._qe_dict[qe]["time"] % 4 == 0 ): # Sample length must be a multiple of 4 wait_duration += 4 - self._qe_dict[qe]["time"] % 4 self.wait(4 - self._qe_dict[qe]["time"] % 4, qe) @@ -164,39 +242,21 @@ def __exit__(self, exc_type, exc_value, exc_traceback): ] ) - # Generates new Op, pulse, and waveform for each qe to be added in the original config file - - self._config["elements"][qe]["operations"][ - f"baked_Op_{self._ctr}" - ] = f"{qe}_baked_pulse_{self._ctr}" - if "I" in qe_samples: - self._config["pulses"][f"{qe}_baked_pulse_{self._ctr}"] = { - "operation": "control", - "length": len(qe_samples["I"]), - "waveforms": { - "I": f"{qe}_baked_wf_I_{self._ctr}", - "Q": f"{qe}_baked_wf_Q_{self._ctr}", - }, - } - self._config["waveforms"][f"{qe}_baked_wf_I_{self._ctr}"] = { - "type": "arbitrary", - "samples": qe_samples["I"], - } - self._config["waveforms"][f"{qe}_baked_wf_Q_{self._ctr}"] = { - "type": "arbitrary", - "samples": qe_samples["Q"], - } - - elif "single" in qe_samples: - self._config["pulses"][f"{qe}_baked_pulse_{self._ctr}"] = { - "operation": "control", - "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["single"], - } + if self.update_config: + self._update_config(qe, qe_samples) + + if "mixInputs" in elements[qe]: + self.override_waveforms_dict["waveforms"][ + f"{qe}_baked_wf_I_{self._ctr}" + ] = qe_samples["I"] + self.override_waveforms_dict["waveforms"][ + f"{qe}_baked_wf_Q_{self._ctr}" + ] = qe_samples["Q"] + + elif "singleInput" in elements[qe]: + self.override_waveforms_dict["waveforms"][ + f"{qe}_baked_wf_{self._ctr}" + ] = qe_samples["single"] def _get_samples(self, pulse: str) -> Union[List[float], List[List]]: """ @@ -233,6 +293,9 @@ def _get_samples(self, pulse: str) -> Union[List[float], List[List]]: except KeyError: raise KeyError(f"No waveforms found for pulse {pulse}") + def get_baking_index(self): + return self._ctr + def get_current_length(self, qe: str): """ Retrieve within the baking the current length of the waveform being created (within the baking) @@ -256,6 +319,24 @@ def _get_pulse_index(self, qe): def get_qe_set(self): return self._qe_set + def get_waveforms_dict(self): + return self.override_waveforms_dict + + def delete_baked_Op(self, qe: str): + """ + Delete in the input config of the baking object the associated baked operation and + its associated pulse and waveform(s) for the specified quantum element + :param qe: quantum element + :return: + """ + del self.config["elements"][qe]["operations"][f"baked_Op_{self._ctr}"] + del self.config["pulses"][f"{qe}_baked_pulse_{self._ctr}"] + if "mixInputs" in self.config["elements"][qe]: + del self.config["waveforms"][f"{qe}_baked_wf_I_{self._ctr}"] + del self.config["waveforms"][f"{qe}_baked_wf_Q_{self._ctr}"] + elif "singleInput" in self.config["elements"][qe]: + del self.config["waveforms"][f"{qe}_baked_wf_{self._ctr}"] + def get_Op_name(self, qe: str): """ Get the baked operation issued from the baking object for quantum element qe @@ -744,6 +825,14 @@ def run(self, amp_array=None, trunc_array=None) -> None: qua.frame_rotation(self._qe_dict[qe]["phase"], qe) + def _retrieve_constraint_length(self, baking_index: int = None): + if baking_index is not None: + for pulse in self._local_config["pulses"]: + if pulse.find(f"baked_pulse_{baking_index}") != -1: + return self._local_config["pulses"][pulse]["length"] + else: + return None + def deterministic_run(baking_list, j): """