From 539c59152b4020fdc5718523095cea0811c8e5ba Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sun, 25 Aug 2024 12:59:42 +0300 Subject: [PATCH 1/9] feat: handle waveforms that are not multiple of 4ns --- src/qibolab/instruments/qm/config/pulses.py | 29 ++++++++++++++++++--- src/qibolab/instruments/qm/controller.py | 8 ------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/qibolab/instruments/qm/config/pulses.py b/src/qibolab/instruments/qm/config/pulses.py index 3b808ace3e..0d53931a14 100644 --- a/src/qibolab/instruments/qm/config/pulses.py +++ b/src/qibolab/instruments/qm/config/pulses.py @@ -2,6 +2,7 @@ from typing import Union import numpy as np +import numpy.typing as npt from qibolab.pulses import Pulse, Rectangular from qibolab.pulses.modulation import rotate, wrap_phase @@ -24,6 +25,25 @@ def operation(pulse): return str(hash(pulse)) +def baked_duration(duration: int) -> int: + if duration < 16: + return 16 + elif duration % 4 != 0: + return duration - (duration % 4) + 4 + return duration + + +def bake(waveforms: npt.NDArray) -> npt.NDArray: + """Handle waveforms with length that is not multiple of 4.""" + ncomponents, duration = waveforms.shape + new_duration = baked_duration(duration) + if new_duration == duration: + return waveforms + pad_len = new_duration - duration + padding = np.zeros((ncomponents, pad_len), dtype=waveforms.dtype) + return np.concatenate((waveforms, padding), axis=1) + + @dataclass(frozen=True) class ConstantWaveform: sample: float @@ -47,9 +67,10 @@ class ArbitraryWaveform: def from_pulse(cls, pulse: Pulse): original_waveforms = pulse.envelopes(SAMPLING_RATE) rotated_waveforms = rotate(original_waveforms, pulse.relative_phase) + baked_waveforms = bake(rotated_waveforms) return { - "I": cls(rotated_waveforms[0]), - "Q": cls(rotated_waveforms[1]), + "I": cls(baked_waveforms[0]), + "Q": cls(baked_waveforms[1]), } @@ -87,7 +108,7 @@ class QmPulse: def from_pulse(cls, pulse: Pulse): op = operation(pulse) return cls( - length=pulse.duration, + length=baked_duration(pulse.duration), waveforms=Waveforms.from_op(op), ) @@ -95,7 +116,7 @@ def from_pulse(cls, pulse: Pulse): def from_dc_pulse(cls, pulse: Pulse): op = operation(pulse) return cls( - length=pulse.duration, + length=baked_duration(pulse.duration), waveforms={"single": op}, ) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index f3578b78e0..b50cca7c9e 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -299,14 +299,6 @@ def configure_channels( def register_pulse(self, channel: Channel, pulse: Pulse) -> str: """Add pulse in the QM ``config`` and return corresponding operation.""" - # if ( - # pulse.duration % 4 != 0 - # or pulse.duration < 16 - # or pulse.id in pulses_to_bake - # ): - # qmpulse = BakedPulse(pulse, element) - # qmpulse.bake(self.config, durations=[pulse.duration]) - # else: name = str(channel.name) if isinstance(channel, DcChannel): return self.config.register_dc_pulse(name, pulse) From eb2c7418c8cb724d51b0fc525e711cd5847b9202 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sun, 25 Aug 2024 14:44:05 +0400 Subject: [PATCH 2/9] fix: divisions with 4 and drop dead code --- src/qibolab/instruments/qm/controller.py | 17 ------------- .../instruments/qm/program/instructions.py | 4 +-- .../instruments/qm/program/sweepers.py | 25 +++---------------- 3 files changed, 6 insertions(+), 40 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index b50cca7c9e..d242bd0ccb 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -71,23 +71,6 @@ def declare_octaves(octaves, host, calibration_path=None): return config -def find_baking_pulses(sweepers): - """Find pulses that require baking because we are sweeping their duration. - - Args: - sweepers (list): List of :class:`qibolab.sweeper.Sweeper` objects. - """ - to_bake = set() - for sweeper in sweepers: - values = sweeper.values - step = values[1] - values[0] if len(values) > 0 else values[0] - if sweeper.parameter is Parameter.duration and step % 4 != 0: - for pulse in sweeper.pulses: - to_bake.add(pulse.id) - - return to_bake - - def fetch_results(result, acquisitions): """Fetches results from an executed experiment. diff --git a/src/qibolab/instruments/qm/program/instructions.py b/src/qibolab/instruments/qm/program/instructions.py index 83a438c9c4..c413466673 100644 --- a/src/qibolab/instruments/qm/program/instructions.py +++ b/src/qibolab/instruments/qm/program/instructions.py @@ -20,7 +20,7 @@ def _delay(pulse: Delay, element: str, parameters: Parameters): if parameters.duration is None: duration = int(pulse.duration) // 4 else: - duration = parameters.duration + duration = parameters.duration / 4 qua.wait(duration + 1, element) @@ -28,7 +28,7 @@ def _play_multiple_waveforms(element: str, parameters: Parameters): """Sweeping pulse duration using distinctly uploaded waveforms.""" with qua.switch_(parameters.duration, unsafe=True): for value, sweep_op in parameters.pulses: - with qua.case_(value // 4): + with qua.case_(value): qua.play(sweep_op, element) diff --git a/src/qibolab/instruments/qm/program/sweepers.py b/src/qibolab/instruments/qm/program/sweepers.py index 342a048ec6..9343587006 100644 --- a/src/qibolab/instruments/qm/program/sweepers.py +++ b/src/qibolab/instruments/qm/program/sweepers.py @@ -46,22 +46,6 @@ def check_max_offset(offset: Optional[float], max_offset: float = MAX_OFFSET): ) -# def _update_baked_pulses(sweeper, qmsequence, config): -# """Updates baked pulse if duration sweeper is used.""" -# qmpulse = qmsequence.pulse_to_qmpulse[sweeper.pulses[0].id] -# is_baked = isinstance(qmpulse, BakedPulse) -# for pulse in sweeper.pulses: -# qmpulse = qmsequence.pulse_to_qmpulse[pulse.id] -# if isinstance(qmpulse, BakedPulse): -# if not is_baked: -# raise_error( -# TypeError, -# "Duration sweeper cannot contain both baked and not baked pulses.", -# ) -# values = np.array(sweeper.values).astype(int) -# qmpulse.bake(config, values) - - def _frequency( channels: list[Channel], values: npt.NDArray, @@ -100,9 +84,6 @@ def _amplitude( raise_error(ValueError, "Amplitude sweep values are >2 which is not supported.") for pulse in pulses: - # if isinstance(instruction, Bake): - # instructions.update_kwargs(instruction, amplitude=a) - # else: args.parameters[operation(pulse)].amplitude = qua.amp(variable) @@ -145,7 +126,6 @@ def _duration( configs: dict[str, Config], args: ExecutionArguments, ): - # TODO: Handle baked pulses for pulse in pulses: args.parameters[operation(pulse)].duration = variable @@ -157,6 +137,10 @@ def normalize_phase(values): def normalize_duration(values): """Convert duration from ns to clock cycles (clock cycle = 4ns).""" + if not all(values % 4 == 0): + raise ValueError( + "Cannot use interpolated duration sweeper for durations that are not multiple of 4ns. Please use normal duration sweeper." + ) return (values // 4).astype(int) @@ -168,7 +152,6 @@ def normalize_duration(values): NORMALIZERS = { Parameter.relative_phase: normalize_phase, - Parameter.duration: normalize_duration, Parameter.duration_interpolated: normalize_duration, } """Functions to normalize sweeper values. From 4b75906732bd599033a586df5043a490d539e117 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sun, 25 Aug 2024 14:51:45 +0400 Subject: [PATCH 3/9] chore: raise error for non-integer durations --- src/qibolab/instruments/qm/controller.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index d242bd0ccb..c07c24c54e 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -297,6 +297,11 @@ def register_pulses(self, configs: dict[str, Config], sequence: PulseSequence): acquisitions (dict): Map from measurement instructions to acquisition objects. """ for channel_id, pulse in sequence: + if hasattr(pulse, "duration") and not pulse.duration.is_integer(): + raise ValueError( + f"Quantum Machines cannot play pulse with duration {pulse.duration}. " + "Only integer duration in ns is supported." + ) if isinstance(pulse, Pulse): channel = self.channels[str(channel_id)].logical_channel self.register_pulse(channel, pulse) From a9a135c3a3d51a22bd273a0f1372ee0fcedd4f39 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sun, 25 Aug 2024 14:57:22 +0400 Subject: [PATCH 4/9] chore: update error --- src/qibolab/instruments/qm/controller.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index c07c24c54e..b5672ad3d4 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -297,7 +297,11 @@ def register_pulses(self, configs: dict[str, Config], sequence: PulseSequence): acquisitions (dict): Map from measurement instructions to acquisition objects. """ for channel_id, pulse in sequence: - if hasattr(pulse, "duration") and not pulse.duration.is_integer(): + if ( + hasattr(pulse, "duration") + and isinstance(pulse.duration, float) + and not pulse.duration.is_integer() + ): raise ValueError( f"Quantum Machines cannot play pulse with duration {pulse.duration}. " "Only integer duration in ns is supported." From 4c1be639b73379a514f41542fd83470e7da6e200 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sun, 25 Aug 2024 21:17:47 +0400 Subject: [PATCH 5/9] fix: not devide by 4 twice when interpolated --- src/qibolab/instruments/qm/program/arguments.py | 1 + .../instruments/qm/program/instructions.py | 2 ++ src/qibolab/instruments/qm/program/sweepers.py | 15 ++++++++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/qibolab/instruments/qm/program/arguments.py b/src/qibolab/instruments/qm/program/arguments.py index 13637e2be2..571e1a3cca 100644 --- a/src/qibolab/instruments/qm/program/arguments.py +++ b/src/qibolab/instruments/qm/program/arguments.py @@ -17,6 +17,7 @@ class Parameters: amplitude: Optional[_Variable] = None phase: Optional[_Variable] = None pulses: list[tuple[float, str]] = field(default_factory=list) + interpolated: bool = False @dataclass diff --git a/src/qibolab/instruments/qm/program/instructions.py b/src/qibolab/instruments/qm/program/instructions.py index c413466673..43db71b0b9 100644 --- a/src/qibolab/instruments/qm/program/instructions.py +++ b/src/qibolab/instruments/qm/program/instructions.py @@ -19,6 +19,8 @@ def _delay(pulse: Delay, element: str, parameters: Parameters): # TODO: How to play delays on multiple elements? if parameters.duration is None: duration = int(pulse.duration) // 4 + elif parameters.interpolated: + duration = parameters.duration else: duration = parameters.duration / 4 qua.wait(duration + 1, element) diff --git a/src/qibolab/instruments/qm/program/sweepers.py b/src/qibolab/instruments/qm/program/sweepers.py index 9343587006..0c882d9b2b 100644 --- a/src/qibolab/instruments/qm/program/sweepers.py +++ b/src/qibolab/instruments/qm/program/sweepers.py @@ -130,6 +130,19 @@ def _duration( args.parameters[operation(pulse)].duration = variable +def _duration_interpolated( + pulses: list[Pulse], + values: npt.NDArray, + variable: _Variable, + configs: dict[str, Config], + args: ExecutionArguments, +): + for pulse in pulses: + params = args.parameters[operation(pulse)] + params.duration = variable + params.interpolated = True + + def normalize_phase(values): """Normalize phase from [0, 2pi] to [0, 1].""" return values / (2 * np.pi) @@ -163,7 +176,7 @@ def normalize_duration(values): Parameter.frequency: _frequency, Parameter.amplitude: _amplitude, Parameter.duration: _duration, - Parameter.duration_interpolated: _duration, + Parameter.duration_interpolated: _duration_interpolated, Parameter.relative_phase: _relative_phase, Parameter.bias: _bias, } From 3a4ae0e5b805eec89a2ec8905565280c73e95704 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sun, 25 Aug 2024 22:19:18 +0400 Subject: [PATCH 6/9] fix: play flux pulse --- src/qibolab/instruments/qm/config/config.py | 8 +++++--- src/qibolab/instruments/qm/program/instructions.py | 4 ++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/qibolab/instruments/qm/config/config.py b/src/qibolab/instruments/qm/config/config.py index 3e53a8d64a..e850332522 100644 --- a/src/qibolab/instruments/qm/config/config.py +++ b/src/qibolab/instruments/qm/config/config.py @@ -109,9 +109,11 @@ def register_waveforms( else: qmpulse = QmAcquisition.from_pulse(pulse, element) waveforms = waveforms_from_pulse(pulse) - modes = ["I"] if dc else ["I", "Q"] - for mode in modes: - self.waveforms[getattr(qmpulse.waveforms, mode)] = waveforms[mode] + if dc: + self.waveforms[qmpulse.waveforms["single"]] = waveforms["I"] + else: + for mode in ["I", "Q"]: + self.waveforms[getattr(qmpulse.waveforms, mode)] = waveforms[mode] return qmpulse def register_iq_pulse(self, element: str, pulse: Pulse): diff --git a/src/qibolab/instruments/qm/program/instructions.py b/src/qibolab/instruments/qm/program/instructions.py index 43db71b0b9..e4c659941c 100644 --- a/src/qibolab/instruments/qm/program/instructions.py +++ b/src/qibolab/instruments/qm/program/instructions.py @@ -6,6 +6,7 @@ from qibolab.components import Config from qibolab.execution_parameters import AcquisitionType, ExecutionParameters +from qibolab.identifier import ChannelType from qibolab.pulses import Align, Delay, Pulse, VirtualZ from qibolab.sweeper import ParallelSweepers @@ -70,6 +71,9 @@ def play(args: ExecutionArguments): processed_aligns = set() for channel_id, pulse in args.sequence: + if channel_id.channel_type is ChannelType.ACQUISITION: + continue + element = str(channel_id) op = operation(pulse) params = args.parameters[op] From 7e03145c7b60b98c3a7e13300b6411b6722a074f Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 27 Aug 2024 19:22:37 +0400 Subject: [PATCH 7/9] chore: simplify baking helper methods --- src/qibolab/instruments/qm/config/pulses.py | 26 +++++++-------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/qibolab/instruments/qm/config/pulses.py b/src/qibolab/instruments/qm/config/pulses.py index 0d53931a14..069b613d3b 100644 --- a/src/qibolab/instruments/qm/config/pulses.py +++ b/src/qibolab/instruments/qm/config/pulses.py @@ -2,7 +2,6 @@ from typing import Union import numpy as np -import numpy.typing as npt from qibolab.pulses import Pulse, Rectangular from qibolab.pulses.modulation import rotate, wrap_phase @@ -26,22 +25,13 @@ def operation(pulse): def baked_duration(duration: int) -> int: - if duration < 16: - return 16 - elif duration % 4 != 0: - return duration - (duration % 4) + 4 - return duration + """Calculate waveform length after pulse baking. - -def bake(waveforms: npt.NDArray) -> npt.NDArray: - """Handle waveforms with length that is not multiple of 4.""" - ncomponents, duration = waveforms.shape - new_duration = baked_duration(duration) - if new_duration == duration: - return waveforms - pad_len = new_duration - duration - padding = np.zeros((ncomponents, pad_len), dtype=waveforms.dtype) - return np.concatenate((waveforms, padding), axis=1) + QM can only play pulses with length that is >16ns and multiple of + 4ns. Waveforms that don't satisfy these constraints are padded with + zeros. + """ + return int(np.maximum((duration + 3) // 4 * 4, 16)) @dataclass(frozen=True) @@ -67,7 +57,9 @@ class ArbitraryWaveform: def from_pulse(cls, pulse: Pulse): original_waveforms = pulse.envelopes(SAMPLING_RATE) rotated_waveforms = rotate(original_waveforms, pulse.relative_phase) - baked_waveforms = bake(rotated_waveforms) + new_duration = baked_duration(pulse.duration) + pad_len = new_duration - int(pulse.duration) + baked_waveforms = np.pad(rotated_waveforms, ((0, 0), (0, pad_len))) return { "I": cls(baked_waveforms[0]), "Q": cls(baked_waveforms[1]), From dbd35989b67325ed71831ceb373f47daeddc819c Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 27 Aug 2024 19:24:43 +0400 Subject: [PATCH 8/9] chore: drop check if duration is float --- src/qibolab/instruments/qm/controller.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index b5672ad3d4..c07c24c54e 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -297,11 +297,7 @@ def register_pulses(self, configs: dict[str, Config], sequence: PulseSequence): acquisitions (dict): Map from measurement instructions to acquisition objects. """ for channel_id, pulse in sequence: - if ( - hasattr(pulse, "duration") - and isinstance(pulse.duration, float) - and not pulse.duration.is_integer() - ): + if hasattr(pulse, "duration") and not pulse.duration.is_integer(): raise ValueError( f"Quantum Machines cannot play pulse with duration {pulse.duration}. " "Only integer duration in ns is supported." From 60580cea7149c597afe59f73a234a1a6030ad97f Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 27 Aug 2024 19:58:10 +0400 Subject: [PATCH 9/9] fix: force delays to be at least 16ns --- src/qibolab/instruments/qm/config/pulses.py | 3 ++- src/qibolab/instruments/qm/program/instructions.py | 11 ++++++++--- src/qibolab/instruments/qm/program/sweepers.py | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/qibolab/instruments/qm/config/pulses.py b/src/qibolab/instruments/qm/config/pulses.py index 069b613d3b..1940838be5 100644 --- a/src/qibolab/instruments/qm/config/pulses.py +++ b/src/qibolab/instruments/qm/config/pulses.py @@ -71,9 +71,10 @@ def from_pulse(cls, pulse: Pulse): def waveforms_from_pulse(pulse: Pulse) -> Waveform: """Register QM waveforms for a given pulse.""" + needs_baking = pulse.duration < 16 or pulse.duration % 4 != 0 wvtype = ( ConstantWaveform - if isinstance(pulse.envelope, Rectangular) + if isinstance(pulse.envelope, Rectangular) and not needs_baking else ArbitraryWaveform ) return wvtype.from_pulse(pulse) diff --git a/src/qibolab/instruments/qm/program/instructions.py b/src/qibolab/instruments/qm/program/instructions.py index e4c659941c..8a7d264088 100644 --- a/src/qibolab/instruments/qm/program/instructions.py +++ b/src/qibolab/instruments/qm/program/instructions.py @@ -19,12 +19,17 @@ def _delay(pulse: Delay, element: str, parameters: Parameters): # TODO: How to play delays on multiple elements? if parameters.duration is None: - duration = int(pulse.duration) // 4 + duration = max(int(pulse.duration) // 4 + 1, 4) + qua.wait(duration, element) elif parameters.interpolated: - duration = parameters.duration + duration = parameters.duration + 1 + qua.wait(duration, element) else: duration = parameters.duration / 4 - qua.wait(duration + 1, element) + with qua.if_(duration < 4): + qua.wait(4, element) + with qua.else_(): + qua.wait(duration, element) def _play_multiple_waveforms(element: str, parameters: Parameters): diff --git a/src/qibolab/instruments/qm/program/sweepers.py b/src/qibolab/instruments/qm/program/sweepers.py index 0c882d9b2b..efaac0401e 100644 --- a/src/qibolab/instruments/qm/program/sweepers.py +++ b/src/qibolab/instruments/qm/program/sweepers.py @@ -150,9 +150,9 @@ def normalize_phase(values): def normalize_duration(values): """Convert duration from ns to clock cycles (clock cycle = 4ns).""" - if not all(values % 4 == 0): + if any(values < 16) and not all(values % 4 == 0): raise ValueError( - "Cannot use interpolated duration sweeper for durations that are not multiple of 4ns. Please use normal duration sweeper." + "Cannot use interpolated duration sweeper for durations that are not multiple of 4ns or are less than 16ns. Please use normal duration sweeper." ) return (values // 4).astype(int)