Skip to content

Commit

Permalink
Allow pulse.axis_angle=None with IQ channel
Browse files Browse the repository at this point in the history
  • Loading branch information
nulinspiratie committed Mar 21, 2024
1 parent 71265e7 commit b37b3bc
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 41 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions quam/components/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
62 changes: 33 additions & 29 deletions quam/components/pulses.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

__all__ = [
"Pulse",
"BaseReadoutPulse",
"ReadoutPulse",
"StandardReadoutPulse",
"DragPulse",
"SquarePulse",
"SquareReadoutPulse",
Expand Down Expand Up @@ -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()
Expand All @@ -194,21 +197,23 @@ 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}

elif isinstance(waveform, (list, np.ndarray)):
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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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):
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 2 additions & 4 deletions tests/components/pulses/test_pulses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit b37b3bc

Please sign in to comment.