From b87d6fcf7847b22031751ffec1f602a0a8f9c928 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 13 Feb 2024 14:05:54 +0100 Subject: [PATCH 01/12] adding tests --- quam/components/pulses.py | 76 ++++++-------------- tests/components/pulses/__init__.py | 0 tests/components/{ => pulses}/test_pulses.py | 72 +++++++++++++++++++ 3 files changed, 93 insertions(+), 55 deletions(-) create mode 100644 tests/components/pulses/__init__.py rename tests/components/{ => pulses}/test_pulses.py (62%) diff --git a/quam/components/pulses.py b/quam/components/pulses.py index 35d7098e..5aecbca4 100644 --- a/quam/components/pulses.py +++ b/quam/components/pulses.py @@ -352,72 +352,38 @@ class ConstantReadoutPulse(ReadoutPulse): 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 not None, the pulse is meant for an IQ channel (0 is X, pi/2 is Y). - integration_weights_angle (float, optional): The rotation angle for the integration - weights in radians. + integration_weights (list[float], list[tuple[float, int]], optional): The + integration weights, can be either + - a list of floats (one per sample), the length must match the pulse length + - a list of tuples of (weight, length) pairs, the sum of the lengths must + match the pulse length + integration_weights_angle (float, optional): The rotation angle for the + integration weights in radians. """ amplitude: float axis_angle: float = 0 + integration_weights: Union[List[float], List[Tuple[float, int]]] = None integration_weights_angle: float = 0 def integration_weights_function(self) -> List[Tuple[Union[complex, float], int]]: - complex_weight = np.exp(1j * self.integration_weights_angle) - return { - "real": [(complex_weight.real, self.length)], - "imag": [(complex_weight.imag, self.length)], - "minus_real": [(-complex_weight.real, self.length)], - "minus_imag": [(-complex_weight.imag, self.length)], - } - - def waveform_function(self): - if self.axis_angle is None: - return self.amplitude - else: - return self.amplitude * np.exp(-1.0j * self.axis_angle) - - -@quam_dataclass -class ArbitraryWeightsReadoutPulse(ReadoutPulse): - """QuAM component for readout pulse with arbitrary weights - - Args: - length (int): The length of the pulse in samples. - digital_marker (str, list, optional): The digital marker to use for the pulse. - 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 not None, the pulse is meant for an IQ channel (0 degrees is X, pi/2 is Y). - integration_weights_real (list): The real part of the integration weights. - integration_weights_imag (list): The imaginary part of the integration weights. - integration_weights_minus_real (list): The negative real part of the integration - weights. - integration_weights_minus_imag (list): The negative imaginary part of the - integration weights. - """ + from qualang_tools.config import convert_integration_weights - amplitude: float - axis_angle: float = 0 - integration_weights_real: List[float] # cos - integration_weights_imag: List[float] # sin - integration_weights_minus_real: List[float] # -cos - integration_weights_minus_imag: List[float] # -sin + phase = np.exp(1j * self.integration_weights_angle) - def integration_weights_function(self): - from qualang_tools.config import convert_integration_weights + if self.integration_weights is None or not self.integration_weights: + integration_weights = [(1, self.length)] + elif isinstance(self.integration_weights[0], float): + integration_weights = convert_integration_weights(self.integration_weights) + else: + integration_weights = self.integration_weights - # Convert integration weights to tuples [(sample, length), ...] - converted_integration_weights = { - "real": convert_integration_weights(self.integration_weights_real), - "imag": convert_integration_weights(self.integration_weights_imag), - "minus_real": convert_integration_weights( - self.integration_weights_minus_real - ), - "minus_imag": convert_integration_weights( - self.integration_weights_minus_imag - ), + return { + "real": [(phase.real * w, l) for w, l in integration_weights], + "imag": [(phase.imag * w, l) for w, l in integration_weights], + "minus_real": [(-phase.real * w, l) for w, l in integration_weights], + "minus_imag": [(-phase.imag * w, l) for w, l in integration_weights], } - return converted_integration_weights def waveform_function(self): if self.axis_angle is None: diff --git a/tests/components/pulses/__init__.py b/tests/components/pulses/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/components/test_pulses.py b/tests/components/pulses/test_pulses.py similarity index 62% rename from tests/components/test_pulses.py rename to tests/components/pulses/test_pulses.py index 50712d4f..2bc5de84 100644 --- a/tests/components/test_pulses.py +++ b/tests/components/pulses/test_pulses.py @@ -147,3 +147,75 @@ def test_pulse_parent_parent_channel(): channel.operations["pulse"] = pulse assert pulse.parent is channel.operations assert pulse.channel is channel + + +def test_constant_readout_pulse_integration_weights_default(): + pulse = pulses.ConstantReadoutPulse(length=100, amplitude=1) + + weights = pulse.integration_weights_function() + assert weights == { + "real": [(1, 100)], + "imag": [(0, 100)], + "minus_real": [(-1, 100)], + "minus_imag": [(0, 100)], + } + + +def test_constant_readout_pulse_integration_weights_phase_shift(): + pulse = pulses.ConstantReadoutPulse( + length=100, amplitude=1, integration_weights_angle=np.pi / 2 + ) + + weights = pulse.integration_weights_function() + + assert weights == { + "real": [(np.cos(np.pi / 2), 100)], + "imag": [(np.sin(np.pi / 2), 100)], + "minus_real": [(-np.cos(np.pi / 2), 100)], + "minus_imag": [(-np.sin(np.pi / 2), 100)], + } + + +def test_constant_readout_pulse_integration_weights_custom_uncompressed(): + pulse = pulses.ConstantReadoutPulse( + length=100, amplitude=1, integration_weights=[0.4] * 40 + [0.6] * 60 + ) + + weights = pulse.integration_weights_function() + assert weights == { + "real": [(0.4, 40), (0.6, 60)], + "imag": [(0, 40), (0, 60)], + "minus_real": [(-0.4, 40), (-0.6, 60)], + "minus_imag": [(0, 40), (0, 60)], + } + + +def test_constant_readout_pulse_integration_weights_custom_compressed(): + pulse = pulses.ConstantReadoutPulse( + length=100, amplitude=1, integration_weights=[(0.4, 40), (0.6, 60)] + ) + + weights = pulse.integration_weights_function() + assert weights == { + "real": [(0.4, 40), (0.6, 60)], + "imag": [(0, 40), (0, 60)], + "minus_real": [(-0.4, 40), (-0.6, 60)], + "minus_imag": [(0, 40), (0, 60)], + } + + +def test_constant_readout_pulse_integration_weights_custom_compressed_phase(): + pulse = pulses.ConstantReadoutPulse( + length=100, + amplitude=1, + integration_weights=[(0.4, 40), (0.6, 60)], + integration_weights_angle=np.pi / 2, + ) + + weights = pulse.integration_weights_function() + assert weights == { + "real": [(np.cos(np.pi / 2) * 0.4, 40), (np.cos(np.pi / 2) * 0.6, 60)], + "imag": [(np.sin(np.pi / 2) * 0.4, 40), (np.sin(np.pi / 2) * 0.6, 60)], + "minus_real": [(np.cos(np.pi / 2) * 0.4, 40), (np.cos(np.pi / 2) * 0.6, 60)], + "minus_imag": [(np.sin(np.pi / 2) * -0.4, 40), (np.sin(np.pi / 2) * -0.6, 60)], + } From 7bcf4d2d517b46dfb9b1949b98eec222019e943d Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 14 Feb 2024 16:17:39 +0100 Subject: [PATCH 02/12] fixed integration weights tests --- tests/components/pulses/test_pulse_weights.py | 89 +++++++++++++++++++ tests/components/pulses/test_pulses.py | 72 --------------- 2 files changed, 89 insertions(+), 72 deletions(-) create mode 100644 tests/components/pulses/test_pulse_weights.py diff --git a/tests/components/pulses/test_pulse_weights.py b/tests/components/pulses/test_pulse_weights.py new file mode 100644 index 00000000..47955ce3 --- /dev/null +++ b/tests/components/pulses/test_pulse_weights.py @@ -0,0 +1,89 @@ +import numpy as np +from quam.components import pulses + + +def compare_integration_weights(expected_weights, weights): + for key, val in expected_weights.items(): + assert key in weights + assert len(val) == len(weights[key]) + for weight_expected, weight in zip(val, weights[key]): + assert np.isclose(weight_expected[0], weight[0], atol=1e-5) + assert weight_expected[1] == weight[1] + + +def test_constant_readout_pulse_integration_weights_default(): + pulse = pulses.ConstantReadoutPulse(length=100, amplitude=1) + + weights = pulse.integration_weights_function() + expected_weights = { + "real": [(1, 100)], + "imag": [(0, 100)], + "minus_real": [(-1, 100)], + "minus_imag": [(0, 100)], + } + compare_integration_weights(expected_weights, weights) + + +def test_constant_readout_pulse_integration_weights_phase_shift(): + pulse = pulses.ConstantReadoutPulse( + length=100, amplitude=1, integration_weights_angle=np.pi / 2 + ) + + weights = pulse.integration_weights_function() + expected_weights = { + "real": [(np.cos(np.pi / 2), 100)], + "imag": [(np.sin(np.pi / 2), 100)], + "minus_real": [(-np.cos(np.pi / 2), 100)], + "minus_imag": [(-np.sin(np.pi / 2), 100)], + } + compare_integration_weights(expected_weights, weights) + + +def test_constant_readout_pulse_integration_weights_custom_uncompressed(): + pulse = pulses.ConstantReadoutPulse( + length=100, + amplitude=1, + integration_weights=[0.4] * 10 + [0.6] * 15, # units of clock cycle + ) + + weights = pulse.integration_weights_function() + expected_weights = { + "real": [(0.4, 40), (0.6, 60)], + "imag": [(0.0, 40), (0.0, 60)], + "minus_real": [(-0.4, 40), (-0.6, 60)], + "minus_imag": [(0.0, 40), (0.0, 60)], + } + compare_integration_weights(expected_weights, weights) + + +def test_constant_readout_pulse_integration_weights_custom_compressed(): + pulse = pulses.ConstantReadoutPulse( + length=100, amplitude=1, integration_weights=[(0.4, 40), (0.6, 60)] + ) + + weights = pulse.integration_weights_function() + expected_weights = { + "real": [(0.4, 40), (0.6, 60)], + "imag": [(0, 40), (0, 60)], + "minus_real": [(-0.4, 40), (-0.6, 60)], + "minus_imag": [(0, 40), (0, 60)], + } + compare_integration_weights(expected_weights, weights) + + +def test_constant_readout_pulse_integration_weights_custom_compressed_phase(): + pulse = pulses.ConstantReadoutPulse( + length=100, + amplitude=1, + integration_weights=[(0.4, 40), (0.6, 60)], + integration_weights_angle=np.pi / 2, + ) + + weights = pulse.integration_weights_function() + expected_weights = { + "real": [(np.cos(np.pi / 2) * 0.4, 40), (np.cos(np.pi / 2) * 0.6, 60)], + "imag": [(np.sin(np.pi / 2) * 0.4, 40), (np.sin(np.pi / 2) * 0.6, 60)], + "minus_real": [(np.cos(np.pi / 2) * 0.4, 40), (np.cos(np.pi / 2) * 0.6, 60)], + "minus_imag": [(np.sin(np.pi / 2) * -0.4, 40), (np.sin(np.pi / 2) * -0.6, 60)], + } + compare_integration_weights(expected_weights, weights) diff --git a/tests/components/pulses/test_pulses.py b/tests/components/pulses/test_pulses.py index 2bc5de84..50712d4f 100644 --- a/tests/components/pulses/test_pulses.py +++ b/tests/components/pulses/test_pulses.py @@ -147,75 +147,3 @@ def test_pulse_parent_parent_channel(): channel.operations["pulse"] = pulse assert pulse.parent is channel.operations assert pulse.channel is channel - - -def test_constant_readout_pulse_integration_weights_default(): - pulse = pulses.ConstantReadoutPulse(length=100, amplitude=1) - - weights = pulse.integration_weights_function() - assert weights == { - "real": [(1, 100)], - "imag": [(0, 100)], - "minus_real": [(-1, 100)], - "minus_imag": [(0, 100)], - } - - -def test_constant_readout_pulse_integration_weights_phase_shift(): - pulse = pulses.ConstantReadoutPulse( - length=100, amplitude=1, integration_weights_angle=np.pi / 2 - ) - - weights = pulse.integration_weights_function() - - assert weights == { - "real": [(np.cos(np.pi / 2), 100)], - "imag": [(np.sin(np.pi / 2), 100)], - "minus_real": [(-np.cos(np.pi / 2), 100)], - "minus_imag": [(-np.sin(np.pi / 2), 100)], - } - - -def test_constant_readout_pulse_integration_weights_custom_uncompressed(): - pulse = pulses.ConstantReadoutPulse( - length=100, amplitude=1, integration_weights=[0.4] * 40 + [0.6] * 60 - ) - - weights = pulse.integration_weights_function() - assert weights == { - "real": [(0.4, 40), (0.6, 60)], - "imag": [(0, 40), (0, 60)], - "minus_real": [(-0.4, 40), (-0.6, 60)], - "minus_imag": [(0, 40), (0, 60)], - } - - -def test_constant_readout_pulse_integration_weights_custom_compressed(): - pulse = pulses.ConstantReadoutPulse( - length=100, amplitude=1, integration_weights=[(0.4, 40), (0.6, 60)] - ) - - weights = pulse.integration_weights_function() - assert weights == { - "real": [(0.4, 40), (0.6, 60)], - "imag": [(0, 40), (0, 60)], - "minus_real": [(-0.4, 40), (-0.6, 60)], - "minus_imag": [(0, 40), (0, 60)], - } - - -def test_constant_readout_pulse_integration_weights_custom_compressed_phase(): - pulse = pulses.ConstantReadoutPulse( - length=100, - amplitude=1, - integration_weights=[(0.4, 40), (0.6, 60)], - integration_weights_angle=np.pi / 2, - ) - - weights = pulse.integration_weights_function() - assert weights == { - "real": [(np.cos(np.pi / 2) * 0.4, 40), (np.cos(np.pi / 2) * 0.6, 60)], - "imag": [(np.sin(np.pi / 2) * 0.4, 40), (np.sin(np.pi / 2) * 0.6, 60)], - "minus_real": [(np.cos(np.pi / 2) * 0.4, 40), (np.cos(np.pi / 2) * 0.6, 60)], - "minus_imag": [(np.sin(np.pi / 2) * -0.4, 40), (np.sin(np.pi / 2) * -0.6, 60)], - } From 22d473dd5832fd94d15d959b1937764fc7f0c164 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 14 Feb 2024 16:42:26 +0100 Subject: [PATCH 03/12] add measure_accumulated, measure_sliced --- CHANGELOG.md | 2 + quam/components/channels.py | 166 ++++++++++++++++++++++++++++++++---- 2 files changed, 153 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40d1991f..87a1a34a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Added - Added InOutSingleChannel - 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` ### Changed - Changed `InOutIQChannel.input_offset_I/Q` to `InOutIQChannel.opx_input_offset_I/Q` @@ -17,6 +18,7 @@ This happens if channel.id is not set, and channel.parent does not have a name either - `Pulse.axis_angle` is now in radians instead of degrees. - Channel offsets (e.g. `SingleChannel.opx_output_offset`) is None by default (see note in Fixed) +- Replaced `InOutIQChannel.measure` kwargs `I_var` and `Q_var` by `qua_vars` tuple ### Fixed - Don't raise instantiation error when required_type is not a class diff --git a/quam/components/channels.py b/quam/components/channels.py index 1888c95f..45ab2348 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -1,5 +1,5 @@ from dataclasses import field -from typing import ClassVar, Dict, List, Optional, Tuple, Union +from typing import ClassVar, Dict, List, Optional, Sequence, Tuple, Union from quam.components.hardware import FrequencyConverter from quam.components.pulses import Pulse, ReadoutPulse @@ -8,7 +8,17 @@ try: - from qm.qua import align, amp, play, wait, measure, dual_demod, declare, fixed + from qm.qua import ( + align, + amp, + play, + wait, + measure, + dual_demod, + declare, + fixed, + demod, + ) from qm.qua._type_hinting import * except ImportError: print("Warning: qm.qua package not found, pulses cannot be played from QuAM.") @@ -516,30 +526,38 @@ def apply_to_config(self, config: dict): if self.input_gain is not None: controller["analog_inputs"][port]["gain_db"] = self.input_gain - def measure(self, pulse_name: str, I_var=None, Q_var=None, stream=None): + def measure( + self, + pulse_name: str, + qua_vars: Tuple[QuaVariableType, QuaVariableType] = None, + stream=None, + ) -> Tuple[QuaVariableType, QuaVariableType]: """Perform a full dual demodulation measurement on this channel. Args: pulse_name (str): The name of the pulse to play. Should be registered in `self.operations`. - I_var (QuaVariableType): QUA variable to store the I measurement result. - If not provided, a new variable will be declared - Q_var (QuaVariableType): QUA variable to store the Q measurement result. - If not provided, a new variable will be declared + qua_vars (Tuple[QuaVariableType, QuaVariableType], optional): Two QUA + variables to store the I and Q measurement results. If not provided, + new variables will be declared and returned. stream (Optional[StreamType]): The stream to save the measurement result to. If not provided, the raw ADC signal will not be streamed. Returns: - I_var, Q_var: The QUA variables used to store the measurement results. + I, Q: The QUA variables used to store the measurement results. 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] - if I_var is None: - I_var = declare(fixed) - if Q_var is None: - Q_var = declare(fixed) + if qua_vars is not None: + if not isinstance(qua_vars, Sequence) or len(qua_vars) != 2: + raise ValueError( + f"InOutIQChannel.measure received kwarg 'qua_vars' which is not a " + f"tuple of two QUA variables.Received {qua_vars=}" + ) + else: + qua_vars = [declare(fixed) for _ in range(2)] integration_weight_labels = list(pulse.integration_weights_mapping) measure( @@ -551,14 +569,132 @@ def measure(self, pulse_name: str, I_var=None, Q_var=None, stream=None): element_output1="out1", iw2=integration_weight_labels[1], element_output2="out2", - target=I_var, + target=qua_vars[0], ), dual_demod.full( iw1=integration_weight_labels[2], element_output1="out1", iw2=integration_weight_labels[0], element_output2="out2", - target=Q_var, + target=qua_vars[1], + ), + ) + return tuple(qua_vars) + + def measure_accumulated( + self, + pulse_name: str, + segment_length: int, + qua_vars: Tuple[QuaVariableType, ...] = None, + stream=None, + ) -> Tuple[QuaVariableType, QuaVariableType, QuaVariableType, QuaVariableType]: + """Perform an accumulated dual demodulation measurement on this channel. + + Instead of two QUA variables (I and Q), this method returns four variables + (II, IQ, QI, QQ) + + Args: + pulse_name (str): The name of the pulse to play. Should be registered in + `self.operations`. + segment_length (int): The length of the segment to accumulate the + measurement. + qua_vars (Tuple[QuaVariableType, ...], optional): Four QUA + variables to store the II, IQ, QI, QQ measurement results. + If not provided, new variables will be declared and returned. + stream (Optional[StreamType]): The stream to save the measurement result to. + If not provided, the raw ADC signal will not be streamed. + + Returns: + II, IQ, QI, QQ: The QUA variables used to store the measurement results. + 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] + + if qua_vars is not None: + if not isinstance(qua_vars, Sequence) or len(qua_vars) != 4: + raise ValueError( + f"InOutIQChannel.measure_accumulated received kwarg 'qua_vars' " + f"which is not a tuple of four QUA variables.Received {qua_vars=}" + ) + else: + qua_vars = [declare(fixed) for _ in range(4)] + + integration_weight_labels = list(pulse.integration_weights_mapping) + measure( + pulse_name, + self.name, + stream, + demod.accumulated( + integration_weight_labels[0], qua_vars[0], segment_length, "out1" + ), + demod.accumulated( + integration_weight_labels[1], qua_vars[1], segment_length, "out2" + ), + demod.accumulated( + integration_weight_labels[2], qua_vars[2], segment_length, "out1" + ), + demod.accumulated( + integration_weight_labels[0], qua_vars[3], segment_length, "out2" + ), + ) + return tuple(qua_vars) + + def measure_sliced( + self, + pulse_name: str, + segment_length: int, + qua_vars: Tuple[QuaVariableType, ...] = None, + stream=None, + ) -> Tuple[QuaVariableType, QuaVariableType, QuaVariableType, QuaVariableType]: + """Perform a sliced dual demodulation measurement on this channel. + + Instead of two QUA variables (I and Q), this method returns four variables + (II, IQ, QI, QQ) + + Args: + pulse_name (str): The name of the pulse to play. Should be registered in + `self.operations`. + segment_length (int): The length of the segment to accumulate the + measurement. + qua_vars (Tuple[QuaVariableType, ...], optional): Four QUA + variables to store the II, IQ, QI, QQ measurement results. + If not provided, new variables will be declared and returned. + stream (Optional[StreamType]): The stream to save the measurement result to. + If not provided, the raw ADC signal will not be streamed. + + Returns: + II, IQ, QI, QQ: The QUA variables used to store the measurement results. + 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] + + if qua_vars is not None: + if not isinstance(qua_vars, Sequence) or len(qua_vars) != 4: + raise ValueError( + f"InOutIQChannel.measure_sliced received kwarg 'qua_vars' " + f"which is not a tuple of four QUA variables.Received {qua_vars=}" + ) + else: + qua_vars = [declare(fixed) for _ in range(4)] + + integration_weight_labels = list(pulse.integration_weights_mapping) + measure( + pulse_name, + self.name, + stream, + demod.sliced( + integration_weight_labels[0], qua_vars[0], segment_length, "out1" + ), + demod.sliced( + integration_weight_labels[1], qua_vars[1], segment_length, "out2" + ), + demod.sliced( + integration_weight_labels[2], qua_vars[2], segment_length, "out1" + ), + demod.sliced( + integration_weight_labels[0], qua_vars[3], segment_length, "out2" ), ) - return I_var, Q_var + return tuple(qua_vars) From f83bac482a36df7ea4245fcc875d75d822c81618 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 14 Feb 2024 16:43:13 +0100 Subject: [PATCH 04/12] add missing change --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a1a34a..60063f57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ This happens if channel.id is not set, and channel.parent does not have a name either - `Pulse.axis_angle` is now in radians instead of degrees. - Channel offsets (e.g. `SingleChannel.opx_output_offset`) is None by default (see note in Fixed) +- Move `quam.components.superconducting_qubits` to `quam.examples.superconducting_qubits` - Replaced `InOutIQChannel.measure` kwargs `I_var` and `Q_var` by `qua_vars` tuple ### Fixed From 88ee003f304c73a94808d5feefab76a730a12e1b Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 14 Feb 2024 16:54:53 +0100 Subject: [PATCH 05/12] added most measurement types --- quam/components/channels.py | 147 +++++++++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 3 deletions(-) diff --git a/quam/components/channels.py b/quam/components/channels.py index 45ab2348..1374df50 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -340,6 +340,147 @@ def apply_to_config(self, config: dict): ) analog_input["offset"] = offset + def measure( + self, + pulse_name: str, + qua_vars: Tuple[QuaVariableType, ...] = None, + stream=None, + ) -> Tuple[QuaVariableType, QuaVariableType]: + """Perform a full demodulation measurement on this channel. + + Args: + pulse_name (str): The name of the pulse to play. Should be registered in + `self.operations`. + qua_vars (Tuple[QuaVariableType, ...], optional): Two QUA + variables to store the I, Q measurement results. + If not provided, new variables will be declared and returned. + stream (Optional[StreamType]): The stream to save the measurement result to. + If not provided, the raw ADC signal will not be streamed. + + Returns: + I, Q: The QUA variables used to store the measurement results. + 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] + + if qua_vars is not None: + if not isinstance(qua_vars, Sequence) or len(qua_vars) != 2: + raise ValueError( + f"InOutSingleChannel.measure received kwarg 'qua_vars' " + f"which is not a tuple of two QUA variables. Received {qua_vars=}" + ) + else: + qua_vars = [declare(fixed) for _ in range(2)] + + integration_weight_labels = list(pulse.integration_weights_mapping) + measure( + pulse_name, + self.name, + stream, + demod.full(integration_weight_labels[0], qua_vars[0], "out1"), + demod.full(integration_weight_labels[1], qua_vars[1], "out2"), + ) + return tuple(qua_vars) + + def measure_accumulated( + self, + pulse_name: str, + segment_length: int, + qua_vars: Tuple[QuaVariableType, ...] = None, + stream=None, + ) -> Tuple[QuaVariableType, QuaVariableType]: + """Perform an accumulated demodulation measurement on this channel. + + Args: + pulse_name (str): The name of the pulse to play. Should be registered in + `self.operations`. + segment_length (int): The length of the segment to accumulate. + qua_vars (Tuple[QuaVariableType, ...], optional): Two QUA + variables to store the I, Q measurement results. + If not provided, new variables will be declared and returned. + stream (Optional[StreamType]): The stream to save the measurement result to. + If not provided, the raw ADC signal will not be streamed. + + Returns: + I, Q: The QUA variables used to store the measurement results. + 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] + + if qua_vars is not None: + if not isinstance(qua_vars, Sequence) or len(qua_vars) != 2: + raise ValueError( + f"InOutSingleChannel.measure_accumulated received kwarg 'qua_vars' " + f"which is not a tuple of two QUA variables. Received {qua_vars=}" + ) + else: + qua_vars = [declare(fixed) for _ in range(2)] + + integration_weight_labels = list(pulse.integration_weights_mapping) + measure( + pulse_name, + self.name, + stream, + demod.accumulated( + integration_weight_labels[0], qua_vars[0], segment_length, "out1" + ), + demod.accumulated( + integration_weight_labels[1], qua_vars[1], segment_length, "out2" + ), + ) + return tuple(qua_vars) + + def measure_sliced( + self, + pulse_name: str, + segment_length: int, + qua_vars: Tuple[QuaVariableType, ...] = None, + stream=None, + ) -> Tuple[QuaVariableType, QuaVariableType]: + """Perform an accumulated demodulation measurement on this channel. + + Args: + pulse_name (str): The name of the pulse to play. Should be registered in + `self.operations`. + segment_length (int): The length of the segment to accumulate. + qua_vars (Tuple[QuaVariableType, ...], optional): Two QUA + variables to store the I, Q measurement results. + If not provided, new variables will be declared and returned. + stream (Optional[StreamType]): The stream to save the measurement result to. + If not provided, the raw ADC signal will not be streamed. + + Returns: + I, Q: The QUA variables used to store the measurement results. + 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] + + if qua_vars is not None: + if not isinstance(qua_vars, Sequence) or len(qua_vars) != 2: + raise ValueError( + f"InOutSingleChannel.measure_accumulated received kwarg 'qua_vars' " + f"which is not a tuple of two QUA variables. Received {qua_vars=}" + ) + else: + qua_vars = [declare(fixed) for _ in range(2)] + + integration_weight_labels = list(pulse.integration_weights_mapping) + measure( + pulse_name, + self.name, + stream, + demod.sliced( + integration_weight_labels[0], qua_vars[0], segment_length, "out1" + ), + demod.sliced( + integration_weight_labels[1], qua_vars[1], segment_length, "out2" + ), + ) + return tuple(qua_vars) + @quam_dataclass class IQChannel(Channel): @@ -554,7 +695,7 @@ def measure( if not isinstance(qua_vars, Sequence) or len(qua_vars) != 2: raise ValueError( f"InOutIQChannel.measure received kwarg 'qua_vars' which is not a " - f"tuple of two QUA variables.Received {qua_vars=}" + f"tuple of two QUA variables. Received {qua_vars=}" ) else: qua_vars = [declare(fixed) for _ in range(2)] @@ -615,7 +756,7 @@ def measure_accumulated( if not isinstance(qua_vars, Sequence) or len(qua_vars) != 4: raise ValueError( f"InOutIQChannel.measure_accumulated received kwarg 'qua_vars' " - f"which is not a tuple of four QUA variables.Received {qua_vars=}" + f"which is not a tuple of four QUA variables. Received {qua_vars=}" ) else: qua_vars = [declare(fixed) for _ in range(4)] @@ -674,7 +815,7 @@ def measure_sliced( if not isinstance(qua_vars, Sequence) or len(qua_vars) != 4: raise ValueError( f"InOutIQChannel.measure_sliced received kwarg 'qua_vars' " - f"which is not a tuple of four QUA variables.Received {qua_vars=}" + f"which is not a tuple of four QUA variables. Received {qua_vars=}" ) else: qua_vars = [declare(fixed) for _ in range(4)] From 470d24d91c511bc6643d4ccd044c5533dec48fb4 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 16 Feb 2024 19:30:52 +0100 Subject: [PATCH 06/12] test fix + error msg improvement --- quam/components/channels.py | 2 +- tests/components/channels/test_single_channel.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/quam/components/channels.py b/quam/components/channels.py index 6409fd7c..41c7fe4d 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -334,7 +334,7 @@ def apply_to_config(self, config: Dict[str, dict]) -> None: """ if self.name in config["elements"]: raise ValueError( - f"Cannot add channel {self.name} to the config because it already " + f"Cannot add channel '{self.name}' to the config because it already " f"exists. Existing entry: {config['elements'][self.name]}" ) config["elements"][self.name] = {"operations": self.pulse_mapping} diff --git a/tests/components/channels/test_single_channel.py b/tests/components/channels/test_single_channel.py index 5a8d3074..5fc67973 100644 --- a/tests/components/channels/test_single_channel.py +++ b/tests/components/channels/test_single_channel.py @@ -56,7 +56,7 @@ def test_single_channel_offset(bare_cfg): def test_single_channel_differing_offsets(bare_cfg): channel1 = SingleChannel(id="channel", opx_output=("con1", 1)) - channel2 = SingleChannel(id="channel", opx_output=("con1", 1)) + channel2 = SingleChannel(id="channel2", opx_output=("con1", 1)) cfg = deepcopy(bare_cfg) channel1.apply_to_config(cfg) @@ -83,6 +83,9 @@ def test_single_channel_differing_offsets(bare_cfg): channel1.apply_to_config(cfg) with pytest.raises(ValueError): channel2.apply_to_config(cfg) + + cfg = deepcopy(bare_cfg) + channel1.apply_to_config(cfg) channel2.opx_output_offset = 0.1 + 0.5e-4 channel2.apply_to_config(cfg) assert cfg["controllers"]["con1"]["analog_outputs"][1] == {"offset": 0.1 + 0.5e-4} From dfba4516def1586e2f44bdacaed265a34e312040 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 16 Feb 2024 19:52:02 +0100 Subject: [PATCH 07/12] remove minus sign in readout pulse phase --- quam/components/pulses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quam/components/pulses.py b/quam/components/pulses.py index fbb22765..4d5eb5c5 100644 --- a/quam/components/pulses.py +++ b/quam/components/pulses.py @@ -389,7 +389,7 @@ def waveform_function(self): if self.axis_angle is None: return self.amplitude else: - return self.amplitude * np.exp(-1.0j * self.axis_angle) + return self.amplitude * np.exp(1.0j * self.axis_angle) @quam_dataclass From 308d1c3996e1be8e343e16b561c42735536942d0 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 16 Feb 2024 20:00:11 +0100 Subject: [PATCH 08/12] fixes to get measure working on OPX (+amplitude_scale) --- quam/components/channels.py | 173 +++++++++++++++++++++++++++++++++--- 1 file changed, 159 insertions(+), 14 deletions(-) diff --git a/quam/components/channels.py b/quam/components/channels.py index 9d1f1ec7..bed48209 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -469,6 +469,7 @@ def apply_to_config(self, config: dict): def measure( self, pulse_name: str, + amplitude_scale: Union[float, AmpValuesType] = None, qua_vars: Tuple[QuaVariableType, ...] = None, stream=None, ) -> Tuple[QuaVariableType, QuaVariableType]: @@ -477,6 +478,8 @@ def measure( Args: pulse_name (str): The name of the pulse to play. Should be registered in `self.operations`. + amplitude_scale (float, _PulseAmp): Amplitude scale of the pulse. + Can be either a float, or qua.amp(float). qua_vars (Tuple[QuaVariableType, ...], optional): Two QUA variables to store the I, Q measurement results. If not provided, new variables will be declared and returned. @@ -487,7 +490,12 @@ def measure( I, Q: The QUA variables used to store the measurement results. If provided as input, the same variables will be returned. If not provided, new variables will be declared and returned. + + Raises: + ValueError: If `qua_vars` is provided and is not a tuple of two QUA + variables. """ + pulse: ReadoutPulse = self.operations[pulse_name] if qua_vars is not None: @@ -499,20 +507,27 @@ def measure( else: qua_vars = [declare(fixed) for _ in range(2)] + if amplitude_scale is not None: + if not isinstance(amplitude_scale, _PulseAmp): + amplitude_scale = amp(amplitude_scale) + pulse_name *= amplitude_scale + integration_weight_labels = list(pulse.integration_weights_mapping) measure( pulse_name, self.name, stream, demod.full(integration_weight_labels[0], qua_vars[0], "out1"), - demod.full(integration_weight_labels[1], qua_vars[1], "out2"), + demod.full(integration_weight_labels[1], qua_vars[1], "out1"), ) return tuple(qua_vars) def measure_accumulated( self, pulse_name: str, - segment_length: int, + amplitude_scale: Union[float, AmpValuesType] = None, + num_segments: int = None, + segment_length: int = None, qua_vars: Tuple[QuaVariableType, ...] = None, stream=None, ) -> Tuple[QuaVariableType, QuaVariableType]: @@ -521,7 +536,12 @@ def measure_accumulated( Args: pulse_name (str): The name of the pulse to play. Should be registered in `self.operations`. + amplitude_scale (float, _PulseAmp): Amplitude scale of the pulse. + Can be either a float, or qua.amp(float). + num_segments (int): The number of segments to accumulate. + Should either specify this or `segment_length`. segment_length (int): The length of the segment to accumulate. + Should either specify this or `num_segments`. qua_vars (Tuple[QuaVariableType, ...], optional): Two QUA variables to store the I, Q measurement results. If not provided, new variables will be declared and returned. @@ -532,9 +552,30 @@ def measure_accumulated( I, Q: The QUA variables used to store the measurement results. If provided as input, the same variables will be returned. If not provided, new variables will be declared and returned. + + Raises: + ValueError: If both `num_segments` and `segment_length` are provided, or if + neither are provided. + ValueError: If `qua_vars` is provided and is not a tuple of two QUA + variables. """ pulse: ReadoutPulse = self.operations[pulse_name] + if num_segments is None and segment_length is None: + raise ValueError( + "InOutSingleChannel.measure_accumulated requires either 'segment_length' " + "or 'num_segments' to be provided." + ) + elif num_segments is not None and segment_length is not None: + raise ValueError( + "InOutSingleChannel.measure_accumulated received both 'segment_length' " + "and 'num_segments'. Please provide only one." + ) + elif num_segments is None: + num_segments = int(pulse.length / (4 * segment_length)) # Number of slices + elif segment_length is None: + segment_length = int(pulse.length / (4 * num_segments)) + if qua_vars is not None: if not isinstance(qua_vars, Sequence) or len(qua_vars) != 2: raise ValueError( @@ -542,7 +583,12 @@ def measure_accumulated( f"which is not a tuple of two QUA variables. Received {qua_vars=}" ) else: - qua_vars = [declare(fixed) for _ in range(2)] + qua_vars = [declare(fixed, size=num_segments) for _ in range(2)] + + if amplitude_scale is not None: + if not isinstance(amplitude_scale, _PulseAmp): + amplitude_scale = amp(amplitude_scale) + pulse_name *= amplitude_scale integration_weight_labels = list(pulse.integration_weights_mapping) measure( @@ -553,7 +599,7 @@ def measure_accumulated( integration_weight_labels[0], qua_vars[0], segment_length, "out1" ), demod.accumulated( - integration_weight_labels[1], qua_vars[1], segment_length, "out2" + integration_weight_labels[1], qua_vars[1], segment_length, "out1" ), ) return tuple(qua_vars) @@ -561,7 +607,9 @@ def measure_accumulated( def measure_sliced( self, pulse_name: str, - segment_length: int, + amplitude_scale: Union[float, AmpValuesType] = None, + num_segments: int = None, + segment_length: int = None, qua_vars: Tuple[QuaVariableType, ...] = None, stream=None, ) -> Tuple[QuaVariableType, QuaVariableType]: @@ -570,7 +618,12 @@ def measure_sliced( Args: pulse_name (str): The name of the pulse to play. Should be registered in `self.operations`. + amplitude_scale (float, _PulseAmp): Amplitude scale of the pulse. + Can be either a float, or qua.amp(float). + num_segments (int): The number of segments to accumulate. + Should either specify this or `segment_length`. segment_length (int): The length of the segment to accumulate. + Should either specify this or `num_segments`. qua_vars (Tuple[QuaVariableType, ...], optional): Two QUA variables to store the I, Q measurement results. If not provided, new variables will be declared and returned. @@ -581,17 +634,43 @@ def measure_sliced( I, Q: The QUA variables used to store the measurement results. If provided as input, the same variables will be returned. If not provided, new variables will be declared and returned. + + Raises: + ValueError: If both `num_segments` and `segment_length` are provided, or if + neither are provided. + ValueError: If `qua_vars` is provided and is not a tuple of two QUA + variables. """ pulse: ReadoutPulse = self.operations[pulse_name] + if num_segments is None and segment_length is None: + raise ValueError( + "InOutSingleChannel.measure_sliced requires either 'segment_length' " + "or 'num_segments' to be provided." + ) + elif num_segments is not None and segment_length is not None: + raise ValueError( + "InOutSingleChannel.measure_sliced received both 'segment_length' " + "and 'num_segments'. Please provide only one." + ) + elif num_segments is None: + num_segments = int(pulse.length / (4 * segment_length)) # Number of slices + elif segment_length is None: + segment_length = int(pulse.length / (4 * num_segments)) + if qua_vars is not None: if not isinstance(qua_vars, Sequence) or len(qua_vars) != 2: raise ValueError( - f"InOutSingleChannel.measure_accumulated received kwarg 'qua_vars' " + f"InOutSingleChannel.measure_sliced received kwarg 'qua_vars' " f"which is not a tuple of two QUA variables. Received {qua_vars=}" ) else: - qua_vars = [declare(fixed) for _ in range(2)] + qua_vars = [declare(fixed, size=num_segments) for _ in range(2)] + + if amplitude_scale is not None: + if not isinstance(amplitude_scale, _PulseAmp): + amplitude_scale = amp(amplitude_scale) + pulse_name *= amplitude_scale integration_weight_labels = list(pulse.integration_weights_mapping) measure( @@ -602,7 +681,7 @@ def measure_sliced( integration_weight_labels[0], qua_vars[0], segment_length, "out1" ), demod.sliced( - integration_weight_labels[1], qua_vars[1], segment_length, "out2" + integration_weight_labels[1], qua_vars[1], segment_length, "out1" ), ) return tuple(qua_vars) @@ -643,10 +722,14 @@ class IQChannel(Channel): @property def local_oscillator(self): + if self.frequency_converter_up is None: + return None return self.frequency_converter_up.local_oscillator @property def mixer(self): + if self.frequency_converter_up is None: + return None return self.frequency_converter_up.mixer @property @@ -784,6 +867,7 @@ def apply_to_config(self, config: dict): def measure( self, pulse_name: str, + amplitude_scale: Union[float, AmpValuesType] = None, qua_vars: Tuple[QuaVariableType, QuaVariableType] = None, stream=None, ) -> Tuple[QuaVariableType, QuaVariableType]: @@ -792,6 +876,8 @@ def measure( Args: pulse_name (str): The name of the pulse to play. Should be registered in `self.operations`. + amplitude_scale (float, _PulseAmp): Amplitude scale of the pulse. + Can be either a float, or qua.amp(float). qua_vars (Tuple[QuaVariableType, QuaVariableType], optional): Two QUA variables to store the I and Q measurement results. If not provided, new variables will be declared and returned. @@ -814,6 +900,11 @@ def measure( else: qua_vars = [declare(fixed) for _ in range(2)] + if amplitude_scale is not None: + if not isinstance(amplitude_scale, _PulseAmp): + amplitude_scale = amp(amplitude_scale) + pulse_name *= amplitude_scale + integration_weight_labels = list(pulse.integration_weights_mapping) measure( pulse_name, @@ -839,7 +930,9 @@ def measure( def measure_accumulated( self, pulse_name: str, - segment_length: int, + amplitude_scale: Union[float, AmpValuesType] = None, + num_segments: int = None, + segment_length: int = None, qua_vars: Tuple[QuaVariableType, ...] = None, stream=None, ) -> Tuple[QuaVariableType, QuaVariableType, QuaVariableType, QuaVariableType]: @@ -851,8 +944,13 @@ def measure_accumulated( Args: pulse_name (str): The name of the pulse to play. Should be registered in `self.operations`. + amplitude_scale (float, _PulseAmp): Amplitude scale of the pulse. + Can be either a float, or qua.amp(float). + num_segments (int): The number of segments to accumulate. + Should either specify this or `segment_length`. segment_length (int): The length of the segment to accumulate the measurement. + Should either specify this or `num_segments`. qua_vars (Tuple[QuaVariableType, ...], optional): Four QUA variables to store the II, IQ, QI, QQ measurement results. If not provided, new variables will be declared and returned. @@ -866,14 +964,34 @@ def measure_accumulated( """ pulse: ReadoutPulse = self.operations[pulse_name] + if num_segments is None and segment_length is None: + raise ValueError( + "InOutSingleChannel.measure_accumulated requires either 'segment_length' " + "or 'num_segments' to be provided." + ) + elif num_segments is not None and segment_length is not None: + raise ValueError( + "InOutSingleChannel.measure_accumulated received both 'segment_length' " + "and 'num_segments'. Please provide only one." + ) + elif num_segments is None: + num_segments = int(pulse.length / (4 * segment_length)) # Number of slices + elif segment_length is None: + segment_length = int(pulse.length / (4 * num_segments)) + if qua_vars is not None: if not isinstance(qua_vars, Sequence) or len(qua_vars) != 4: raise ValueError( - f"InOutIQChannel.measure_accumulated received kwarg 'qua_vars' " + f"InOutSingleChannel.measure_accumulated received kwarg 'qua_vars' " f"which is not a tuple of four QUA variables. Received {qua_vars=}" ) else: - qua_vars = [declare(fixed) for _ in range(4)] + qua_vars = [declare(fixed, size=num_segments) for _ in range(4)] + + if amplitude_scale is not None: + if not isinstance(amplitude_scale, _PulseAmp): + amplitude_scale = amp(amplitude_scale) + pulse_name *= amplitude_scale integration_weight_labels = list(pulse.integration_weights_mapping) measure( @@ -898,7 +1016,9 @@ def measure_accumulated( def measure_sliced( self, pulse_name: str, - segment_length: int, + amplitude_scale: Union[float, AmpValuesType] = None, + num_segments: int = None, + segment_length: int = None, qua_vars: Tuple[QuaVariableType, ...] = None, stream=None, ) -> Tuple[QuaVariableType, QuaVariableType, QuaVariableType, QuaVariableType]: @@ -910,8 +1030,13 @@ def measure_sliced( Args: pulse_name (str): The name of the pulse to play. Should be registered in `self.operations`. + amplitude_scale (float, _PulseAmp): Amplitude scale of the pulse. + Can be either a float, or qua.amp(float). + num_segments (int): The number of segments to accumulate. + Should either specify this or `segment_length`. segment_length (int): The length of the segment to accumulate the measurement. + Should either specify this or `num_segments`. qua_vars (Tuple[QuaVariableType, ...], optional): Four QUA variables to store the II, IQ, QI, QQ measurement results. If not provided, new variables will be declared and returned. @@ -925,14 +1050,34 @@ def measure_sliced( """ pulse: ReadoutPulse = self.operations[pulse_name] + if num_segments is None and segment_length is None: + raise ValueError( + "InOutSingleChannel.measure_sliced requires either 'segment_length' " + "or 'num_segments' to be provided." + ) + elif num_segments is not None and segment_length is not None: + raise ValueError( + "InOutSingleChannel.measure_sliced received both 'segment_length' " + "and 'num_segments'. Please provide only one." + ) + elif num_segments is None: + num_segments = int(pulse.length / (4 * segment_length)) # Number of slices + elif segment_length is None: + segment_length = int(pulse.length / (4 * num_segments)) + if qua_vars is not None: if not isinstance(qua_vars, Sequence) or len(qua_vars) != 4: raise ValueError( - f"InOutIQChannel.measure_sliced received kwarg 'qua_vars' " + f"InOutSingleChannel.measure_sliced received kwarg 'qua_vars' " f"which is not a tuple of four QUA variables. Received {qua_vars=}" ) else: - qua_vars = [declare(fixed) for _ in range(4)] + qua_vars = [declare(fixed, size=num_segments) for _ in range(4)] + + if amplitude_scale is not None: + if not isinstance(amplitude_scale, _PulseAmp): + amplitude_scale = amp(amplitude_scale) + pulse_name *= amplitude_scale integration_weight_labels = list(pulse.integration_weights_mapping) measure( From 10518d5b943865be98927368e45fd83c96f51858 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 28 Feb 2024 16:01:16 +0100 Subject: [PATCH 09/12] add QuaVariableType --- quam/components/channels.py | 1 + 1 file changed, 1 insertion(+) diff --git a/quam/components/channels.py b/quam/components/channels.py index 617bb099..4cb0beb7 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -23,6 +23,7 @@ _PulseAmp, AmpValuesType, QuaNumberType, + QuaVariableType, QuaExpressionType, ChirpType, StreamType, From 9667a4e1ee451138a5df2c170220f669a4254f07 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Sat, 2 Mar 2024 19:45:57 +0100 Subject: [PATCH 10/12] Add `StandardReadoutPulse`, change `ConstantReadoutPulse` to `SquareReadoutPulse` --- CHANGELOG.md | 1 + quam/components/pulses.py | 75 ++++++++++++------- .../channels/test_in_out_IQ_channel.py | 6 +- tests/components/pulses/test_pulse_weights.py | 10 +-- 4 files changed, 55 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7831b436..6936379c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Added InOutSingleChannel - 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. ### Changed - Changed `InOutIQChannel.input_offset_I/Q` to `InOutIQChannel.opx_input_offset_I/Q` diff --git a/quam/components/pulses.py b/quam/components/pulses.py index 2a32f702..649dda5e 100644 --- a/quam/components/pulses.py +++ b/quam/components/pulses.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod import numbers +import warnings from typing import Any, ClassVar, Dict, List, Union, Tuple import numpy as np @@ -10,11 +11,13 @@ __all__ = [ "Pulse", "ReadoutPulse", - "ConstantReadoutPulse", + "StandardReadoutPulse", "DragPulse", "SquarePulse", + "SquareReadoutPulse", "GaussianPulse", "FlatTopGaussianPulse", + "ConstantReadoutPulse", ] @@ -52,6 +55,7 @@ class Pulse(QuamComponent): The digital marker label is defined as `"{channel_name}.{pulse_name}.dm"`. """ + operation: ClassVar[str] = "control" length: int id: str = None @@ -340,28 +344,7 @@ def apply_to_config(self, config: dict) -> None: @quam_dataclass -class ConstantReadoutPulse(ReadoutPulse): - """QuAM component for a constant readout pulse. - - Args: - length (int): The length of the pulse in samples. - digital_marker (str, list, optional): The digital marker to use for the pulse. - 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 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 - - a list of floats (one per sample), the length must match the pulse length - - a list of tuples of (weight, length) pairs, the sum of the lengths must - match the pulse length - integration_weights_angle (float, optional): The rotation angle for the - integration weights in radians. - """ - - amplitude: float - axis_angle: float = 0 +class StandardReadoutPulse(ReadoutPulse, ABC): integration_weights: Union[List[float], List[Tuple[float, int]]] = None integration_weights_angle: float = 0 @@ -384,12 +367,6 @@ def integration_weights_function(self) -> List[Tuple[Union[complex, float], int] "minus_imag": [(-phase.imag * w, l) for w, l in integration_weights], } - def waveform_function(self): - if self.axis_angle is None: - return self.amplitude - else: - return self.amplitude * np.exp(1.0j * self.axis_angle) - @quam_dataclass class DragPulse(Pulse): @@ -470,6 +447,46 @@ def waveform_function(self): return waveform +@quam_dataclass +class SquareReadoutPulse(StandardReadoutPulse, SquarePulse): + """QuAM component for a constant readout pulse. + + Args: + length (int): The length of the pulse in samples. + digital_marker (str, list, optional): The digital marker to use for the pulse. + 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 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 + - a list of floats (one per sample), the length must match the pulse length + - a list of tuples of (weight, length) pairs, the sum of the lengths must + match the pulse length + integration_weights_angle (float, optional): The rotation angle for the + 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): + def __post_init__(self) -> None: + warnings.warn( + "ConstantReadoutPulse is deprecated. Use SquareReadoutPulse instead.", + DeprecationWarning, + ) + return super().__post_init__() + + @quam_dataclass class GaussianPulse(Pulse): """Gaussian pulse QuAM component. diff --git a/tests/components/channels/test_in_out_IQ_channel.py b/tests/components/channels/test_in_out_IQ_channel.py index 0ccef9a3..843e6742 100644 --- a/tests/components/channels/test_in_out_IQ_channel.py +++ b/tests/components/channels/test_in_out_IQ_channel.py @@ -120,7 +120,7 @@ def test_readout_resonator_with_readout(): mixer=Mixer(), local_oscillator=LocalOscillator(frequency=5e9) ), ) - readout_resonator.operations["readout"] = pulses.ConstantReadoutPulse( + readout_resonator.operations["readout"] = pulses.SquareReadoutPulse( amplitude=0.1, length=1000 ) @@ -138,7 +138,7 @@ def test_readout_resonator_with_readout(): "id": 1, "operations": { "readout": { - "__class__": "quam.components.pulses.ConstantReadoutPulse", + "__class__": "quam.components.pulses.SquareReadoutPulse", "amplitude": 0.1, "length": 1000, } @@ -246,7 +246,7 @@ def test_channel_measure(mocker): mixer=Mixer(), local_oscillator=LocalOscillator(frequency=5e9) ), ) - readout_resonator.operations["readout"] = pulses.ConstantReadoutPulse( + readout_resonator.operations["readout"] = pulses.SquareReadoutPulse( amplitude=0.1, length=1000 ) diff --git a/tests/components/pulses/test_pulse_weights.py b/tests/components/pulses/test_pulse_weights.py index 47955ce3..e8fde587 100644 --- a/tests/components/pulses/test_pulse_weights.py +++ b/tests/components/pulses/test_pulse_weights.py @@ -12,7 +12,7 @@ def compare_integration_weights(expected_weights, weights): def test_constant_readout_pulse_integration_weights_default(): - pulse = pulses.ConstantReadoutPulse(length=100, amplitude=1) + pulse = pulses.SquareReadoutPulse(length=100, amplitude=1) weights = pulse.integration_weights_function() expected_weights = { @@ -25,7 +25,7 @@ def test_constant_readout_pulse_integration_weights_default(): def test_constant_readout_pulse_integration_weights_phase_shift(): - pulse = pulses.ConstantReadoutPulse( + pulse = pulses.SquareReadoutPulse( length=100, amplitude=1, integration_weights_angle=np.pi / 2 ) @@ -40,7 +40,7 @@ def test_constant_readout_pulse_integration_weights_phase_shift(): def test_constant_readout_pulse_integration_weights_custom_uncompressed(): - pulse = pulses.ConstantReadoutPulse( + pulse = pulses.SquareReadoutPulse( length=100, amplitude=1, integration_weights=[0.4] * 10 + [0.6] * 15, # units of clock cycle @@ -57,7 +57,7 @@ def test_constant_readout_pulse_integration_weights_custom_uncompressed(): def test_constant_readout_pulse_integration_weights_custom_compressed(): - pulse = pulses.ConstantReadoutPulse( + pulse = pulses.SquareReadoutPulse( length=100, amplitude=1, integration_weights=[(0.4, 40), (0.6, 60)] ) @@ -72,7 +72,7 @@ def test_constant_readout_pulse_integration_weights_custom_compressed(): def test_constant_readout_pulse_integration_weights_custom_compressed_phase(): - pulse = pulses.ConstantReadoutPulse( + pulse = pulses.SquareReadoutPulse( length=100, amplitude=1, integration_weights=[(0.4, 40), (0.6, 60)], From d24df039c48c34e6451e639150d9a02526389126 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Sat, 2 Mar 2024 19:49:40 +0100 Subject: [PATCH 11/12] Add docstring --- quam/components/pulses.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/quam/components/pulses.py b/quam/components/pulses.py index 649dda5e..025f8d87 100644 --- a/quam/components/pulses.py +++ b/quam/components/pulses.py @@ -278,7 +278,10 @@ def apply_to_config(self, config: dict) -> None: @quam_dataclass class ReadoutPulse(Pulse, ABC): - """QuAM abstract base component for a readout pulse. + """QuAM abstract base component for a general readout pulse. + + Readout pulse classes should usually inherit from `StandardReadoutPulse`, the + exception being when a custom integration weights function is required. Args: length (int): The length of the pulse in samples. @@ -345,6 +348,29 @@ def apply_to_config(self, config: dict) -> None: @quam_dataclass class StandardReadoutPulse(ReadoutPulse, ABC): + """QuAM abstract base component for most readout pulses. + + This class is a subclass of `ReadoutPulse` and should be used for most readout + pulses. It provides a default implementation of the `integration_weights_function` + method, which is suitable for most cases. + + Args: + length (int): The length of the pulse in samples. + digital_marker (str, list, optional): The digital marker to use for the pulse. + 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 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 + - a list of floats (one per sample), the length must match the pulse length + - a list of tuples of (weight, length) pairs, the sum of the lengths must + match the pulse length + integration_weights_angle (float, optional): The rotation angle for the + integration weights in radians. + """ + integration_weights: Union[List[float], List[Tuple[float, int]]] = None integration_weights_angle: float = 0 @@ -449,7 +475,7 @@ def waveform_function(self): @quam_dataclass class SquareReadoutPulse(StandardReadoutPulse, SquarePulse): - """QuAM component for a constant readout pulse. + """QuAM component for a square readout pulse. Args: length (int): The length of the pulse in samples. From b37b3bc0537c95848abef3dfc497016d24723de5 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 21 Mar 2024 15:44:48 +0100 Subject: [PATCH 12/12] 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)