From b37b3bc0537c95848abef3dfc497016d24723de5 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 21 Mar 2024 15:44:48 +0100 Subject: [PATCH] Allow pulse.axis_angle=None with IQ channel --- CHANGELOG.md | 4 +- quam/components/channels.py | 14 +++--- quam/components/pulses.py | 62 ++++++++++++++------------ tests/components/pulses/test_pulses.py | 6 +-- 4 files changed, 45 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a68214bb..e9e590d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,10 @@ ### Added - Add optional `config_settings` property to quam components indicating that they should be called before/after other components when generating QUA configuration - Added `InOutIQChannel.measure_accumulated/sliced` -- Added `StandardReadoutPulse`. All readout pulses can now be created simply by inheriting from the `StandardReadoutPulse` and the non-readout variant. +- Added `ReadoutPulse`. All readout pulses can now be created simply by inheriting from the `ReadoutPulse` and the non-readout variant. +### Changed +- Pulses with `pulse.axis_angle = None` are now compatible with an `IQChannel` as all signal on the I port. ## [0.3.0] ### Added diff --git a/quam/components/channels.py b/quam/components/channels.py index 7152a269..44b861e9 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -3,7 +3,7 @@ import warnings from quam.components.hardware import BaseFrequencyConverter, Mixer, LocalOscillator -from quam.components.pulses import Pulse, ReadoutPulse +from quam.components.pulses import Pulse, BaseReadoutPulse from quam.core import QuamComponent, quam_dataclass from quam.core.quam_classes import QuamDict from quam.utils import string_reference as str_ref @@ -544,7 +544,7 @@ def measure( variables. """ - pulse: ReadoutPulse = self.operations[pulse_name] + pulse: BaseReadoutPulse = self.operations[pulse_name] if qua_vars is not None: if not isinstance(qua_vars, Sequence) or len(qua_vars) != 2: @@ -607,7 +607,7 @@ def measure_accumulated( ValueError: If `qua_vars` is provided and is not a tuple of two QUA variables. """ - pulse: ReadoutPulse = self.operations[pulse_name] + pulse: BaseReadoutPulse = self.operations[pulse_name] if num_segments is None and segment_length is None: raise ValueError( @@ -689,7 +689,7 @@ def measure_sliced( ValueError: If `qua_vars` is provided and is not a tuple of two QUA variables. """ - pulse: ReadoutPulse = self.operations[pulse_name] + pulse: BaseReadoutPulse = self.operations[pulse_name] if num_segments is None and segment_length is None: raise ValueError( @@ -978,7 +978,7 @@ def measure( If provided as input, the same variables will be returned. If not provided, new variables will be declared and returned. """ - pulse: ReadoutPulse = self.operations[pulse_name] + pulse: BaseReadoutPulse = self.operations[pulse_name] if qua_vars is not None: if not isinstance(qua_vars, Sequence) or len(qua_vars) != 2: @@ -1051,7 +1051,7 @@ def measure_accumulated( If provided as input, the same variables will be returned. If not provided, new variables will be declared and returned. """ - pulse: ReadoutPulse = self.operations[pulse_name] + pulse: BaseReadoutPulse = self.operations[pulse_name] if num_segments is None and segment_length is None: raise ValueError( @@ -1137,7 +1137,7 @@ def measure_sliced( If provided as input, the same variables will be returned. If not provided, new variables will be declared and returned. """ - pulse: ReadoutPulse = self.operations[pulse_name] + pulse: BaseReadoutPulse = self.operations[pulse_name] if num_segments is None and segment_length is None: raise ValueError( diff --git a/quam/components/pulses.py b/quam/components/pulses.py index 025f8d87..459b704f 100644 --- a/quam/components/pulses.py +++ b/quam/components/pulses.py @@ -10,8 +10,8 @@ __all__ = [ "Pulse", + "BaseReadoutPulse", "ReadoutPulse", - "StandardReadoutPulse", "DragPulse", "SquarePulse", "SquareReadoutPulse", @@ -182,6 +182,9 @@ def _config_add_waveforms(self, config): ValueError: If the waveform type (single or IQ) does not match the parent channel type (SingleChannel, IQChannel, InOutIQChannel). """ + + from quam.components.channels import SingleChannel, IQChannel + pulse_config = config["pulses"][self.pulse_name] waveform = self.calculate_waveform() @@ -194,6 +197,8 @@ def _config_add_waveforms(self, config): wf_type = "constant" if isinstance(waveform, complex): waveforms = {"I": waveform.real, "Q": waveform.imag} + elif isinstance(self.channel, IQChannel): + waveforms = {"I": waveform, "Q": 0.0} else: waveforms = {"single": waveform} @@ -201,14 +206,14 @@ def _config_add_waveforms(self, config): wf_type = "arbitrary" if np.iscomplexobj(waveform): waveforms = {"I": list(waveform.real), "Q": list(waveform.imag)} + elif isinstance(self.channel, IQChannel): + waveforms = {"I": waveform, "Q": np.zeros_like(waveform)} else: waveforms = {"single": list(waveform)} else: raise ValueError("unsupported return type") # Add check that waveform type (single or IQ) matches parent - from quam.components.channels import SingleChannel, IQChannel - if "single" in waveforms and not isinstance(self.channel, SingleChannel): raise ValueError( "Waveform type 'single' not allowed for IQChannel" @@ -277,10 +282,10 @@ def apply_to_config(self, config: dict) -> None: @quam_dataclass -class ReadoutPulse(Pulse, ABC): +class BaseReadoutPulse(Pulse, ABC): """QuAM abstract base component for a general readout pulse. - Readout pulse classes should usually inherit from `StandardReadoutPulse`, the + Readout pulse classes should usually inherit from `ReadoutPulse`, the exception being when a custom integration weights function is required. Args: @@ -347,7 +352,7 @@ def apply_to_config(self, config: dict) -> None: @quam_dataclass -class StandardReadoutPulse(ReadoutPulse, ABC): +class ReadoutPulse(BaseReadoutPulse, ABC): """QuAM abstract base component for most readout pulses. This class is a subclass of `ReadoutPulse` and should be used for most readout @@ -360,7 +365,8 @@ class StandardReadoutPulse(ReadoutPulse, ABC): Default is "ON". amplitude (float): The constant amplitude of the pulse. axis_angle (float, optional): IQ axis angle of the output pulse in radians. - If None (default), the pulse is meant for a single channel. + If None (default), the pulse is meant for a single channel or the I port + of an IQ channel If not None, the pulse is meant for an IQ channel (0 is X, pi/2 is Y). integration_weights (list[float], list[tuple[float, int]], optional): The integration weights, can be either @@ -406,9 +412,10 @@ class DragPulse(Pulse): Args: length (int): The pulse length in ns. - axis_angle (float, optional): IQ axis angle of the pulse in radians. - If None (default), the pulse is meant for a single channel. - If not None, the pulse is meant for an IQ channel (0 degrees is X, pi/2 is Y). + axis_angle (float, optional): IQ axis angle of the output pulse in radians. + If None (default), the pulse is meant for a single channel or the I port + of an IQ channel + If not None, the pulse is meant for an IQ channel (0 is X, pi/2 is Y). amplitude (float): The amplitude in volts. sigma (float): The gaussian standard deviation. alpha (float): The DRAG coefficient. @@ -457,9 +464,10 @@ class SquarePulse(Pulse): length (int): The length of the pulse in samples. digital_marker (str, list, optional): The digital marker to use for the pulse. amplitude (float): The amplitude of the pulse in volts. - axis_angle (float, optional): IQ axis angle of the pulse in radians. - If None (default), the pulse is meant for a single channel. - If not None, the pulse is meant for an IQ channel (0 degrees is X, pi/2 is Y). + axis_angle (float, optional): IQ axis angle of the output pulse in radians. + If None (default), the pulse is meant for a single channel or the I port + of an IQ channel + If not None, the pulse is meant for an IQ channel (0 is X, pi/2 is Y). """ amplitude: float @@ -474,7 +482,7 @@ def waveform_function(self): @quam_dataclass -class SquareReadoutPulse(StandardReadoutPulse, SquarePulse): +class SquareReadoutPulse(ReadoutPulse, SquarePulse): """QuAM component for a square readout pulse. Args: @@ -483,7 +491,8 @@ class SquareReadoutPulse(StandardReadoutPulse, SquarePulse): Default is "ON". amplitude (float): The constant amplitude of the pulse. axis_angle (float, optional): IQ axis angle of the output pulse in radians. - If None (default), the pulse is meant for a single channel. + If None (default), the pulse is meant for a single channel or the I port + of an IQ channel If not None, the pulse is meant for an IQ channel (0 is X, pi/2 is Y). integration_weights (list[float], list[tuple[float, int]], optional): The integration weights, can be either @@ -494,14 +503,7 @@ class SquareReadoutPulse(StandardReadoutPulse, SquarePulse): integration weights in radians. """ - amplitude: float - axis_angle: float = 0 - - def waveform_function(self): - if self.axis_angle is None: - return self.amplitude - else: - return self.amplitude * np.exp(1.0j * self.axis_angle) + ... class ConstantReadoutPulse(SquareReadoutPulse): @@ -522,9 +524,10 @@ class GaussianPulse(Pulse): length (int): The length of the pulse in samples. sigma (float): The standard deviation of the gaussian pulse. Should generally be less than half the length of the pulse. - axis_angle (float, optional): IQ axis angle of the pulse in radians. - If None (default), the pulse is meant for a single channel. - If not None, the pulse is meant for an IQ channel (0 degrees is X, pi/2 is Y). + axis_angle (float, optional): IQ axis angle of the output pulse in radians. + If None (default), the pulse is meant for a single channel or the I port + of an IQ channel + If not None, the pulse is meant for an IQ channel (0 is X, pi/2 is Y). subtracted (bool): If true, returns a subtracted Gaussian, such that the first and last points will be at 0 volts. This reduces high-frequency components due to the initial and final points offset. Default is true. @@ -557,9 +560,10 @@ class FlatTopGaussianPulse(Pulse): Args: length (int): The total length of the pulse in samples. amplitude (float): The amplitude of the pulse in volts. - axis_angle (float, optional): IQ axis angle of the pulse in radians. - If None (default), the pulse is meant for a single channel. - If not None, the pulse is meant for an IQ channel (0 degrees is X, pi/2 is Y). + axis_angle (float, optional): IQ axis angle of the output pulse in radians. + If None (default), the pulse is meant for a single channel or the I port + of an IQ channel + If not None, the pulse is meant for an IQ channel (0 is X, pi/2 is Y). flat_length (int): The length of the pulse's flat top in samples. The rise and fall lengths are calculated from the total length and the flat length. diff --git a/tests/components/pulses/test_pulses.py b/tests/components/pulses/test_pulses.py index 35ae069a..a81c1ea6 100644 --- a/tests/components/pulses/test_pulses.py +++ b/tests/components/pulses/test_pulses.py @@ -78,10 +78,8 @@ def test_single_pulse_IQ_channel(): cfg = {"pulses": {}, "waveforms": {}} pulse = IQ_channel.operations["X180"] - with pytest.raises(ValueError) as exc_info: - pulse.apply_to_config(cfg) - error_message = "Waveform type 'single' not allowed for IQChannel 'IQ'" - assert str(exc_info.value) == error_message + # axis_angle = None translates to all signal on I + pulse.apply_to_config(cfg) pulse.axis_angle = 90 pulse.apply_to_config(cfg)