diff --git a/CHANGELOG.md b/CHANGELOG.md index f79af0c9..ade7fd5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ### Fixed - Change location of port feedforward and feedback filters in config + +## [0.3.7] +### Added +- Added `WaveformPulse` to allow for pre-defined waveforms. + + ## [0.3.6] ### Changed - Modified `MWChannel` to also have `RF_frequency` and `LO_frequency` to match the signature of `IQChannel`. diff --git a/README.md b/README.md index 97bd9e82..a2ad0c67 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,12 @@ from qm import qua # Create a root-level QuAM instance machine = BasicQuAM() -# Add an OPX output channel -channel = SingleChannel(opx_output=("con1", 1)) -machine.channels["output"] = channel +# Add a qubit connected to an OPX output channel +qubit = SingleChannel(opx_output=("con1", 1)) +machine.channels["qubit"] = qubit # Add a Gaussian pulse to the channel -channel.operations["gaussian"] = pulses.GaussianPulse( +qubit.operations["gaussian"] = pulses.GaussianPulse( length=100, # Pulse length in ns amplitude=0.5, # Peak amplitude of Gaussian pulse sigma=20, # Standard deviation of Guassian pulse @@ -43,7 +43,7 @@ channel.operations["gaussian"] = pulses.GaussianPulse( # Play the Gaussian pulse on the channel within a QUA program with qua.program() as prog: - channel.play("gaussian") + qubit.play("gaussian") # Generate the QUA configuration from QuAM qua_configuration = machine.generate_config() diff --git a/docs/components/custom-components.md b/docs/components/custom-components.md index d75f4b7e..3f2ce9dd 100644 --- a/docs/components/custom-components.md +++ b/docs/components/custom-components.md @@ -12,11 +12,12 @@ First create the following folder structure ``` my-quam ├── my_quam +│ └── __init__.py │ └── components │ └── __init__.py └── pyproject.toml ``` -The file `__init__.py` should be empty, and `pyproject.toml` should have the following contents: +The `__init__.py` files should be empty, and `pyproject.toml` should have the following contents: /// details | pyproject.toml ```toml diff --git a/docs/demonstration.md b/docs/demonstration.md index f2b5a1ef..7c244d3f 100644 --- a/docs/demonstration.md +++ b/docs/demonstration.md @@ -63,8 +63,8 @@ for idx in range(num_qubits): # Add resonator channel transmon.resonator = InOutIQChannel( id=idx, - opx_output_I=("con1", 3 * idx + 1), - opx_output_Q=("con1", 3 * idx + 2), + opx_output_I=("con1", 1), + opx_output_Q=("con1", 2), opx_input_I=("con1", 1), opx_input_Q=("con1", 2,), frequency_converter_up=FrequencyConverter( diff --git a/docs/index.md b/docs/index.md index 812146d5..23668a94 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,30 +18,29 @@ QuAM is not just a tool but a gateway to streamlined and efficient quantum compu - **State Management:** Effortlessly save and load your QuAM state, enabling consistent results and reproducibility in experiments. ```python -from quam.components import * +from quam.components import BasicQuAM, SingleChannel, pulses from qm import qua # Create a root-level QuAM instance machine = BasicQuAM() -# Add an OPX output channel -channel = SingleChannel(opx_output=("con1", 1)) -machine.channels["output"] = channel +# Add a qubit connected to an OPX output channel +qubit = SingleChannel(opx_output=("con1", 1)) +machine.channels["qubit"] = qubit # Add a Gaussian pulse to the channel -channel.operations["gaussian"] = pulses.GaussianPulse( - length=100, amplitude=0.5, sigma=20 +qubit.operations["gaussian"] = pulses.GaussianPulse( + length=100, # Pulse length in ns + amplitude=0.5, # Peak amplitude of Gaussian pulse + sigma=20, # Standard deviation of Guassian pulse ) -# Play the Gaussian pulse within a QUA program +# Play the Gaussian pulse on the channel within a QUA program with qua.program() as prog: - channel.play("gaussian") + qubit.play("gaussian") # Generate the QUA configuration from QuAM qua_configuration = machine.generate_config() - -# Save QuAM to a JSON file -machine.save("state.json") ``` diff --git a/pyproject.toml b/pyproject.toml index 773743f8..fe055e15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "quam" -version = "0.3.6" +version = "0.3.7" #dynamic = ["version"] description = "Quantum Abstract Machine (QuAM) facilitates development of abstraction layers in experiments." authors = [ diff --git a/quam/components/pulses.py b/quam/components/pulses.py index e6f48985..e3b2a005 100644 --- a/quam/components/pulses.py +++ b/quam/components/pulses.py @@ -1,7 +1,8 @@ from abc import ABC, abstractmethod +from collections.abc import Iterable import numbers import warnings -from typing import Any, ClassVar, Dict, List, Union, Tuple +from typing import Any, ClassVar, Dict, List, Optional, Union, Tuple import numpy as np from quam.core import QuamComponent, quam_dataclass @@ -12,6 +13,7 @@ "Pulse", "BaseReadoutPulse", "ReadoutPulse", + "WaveformPulse", "DragGaussianPulse", "DragCosinePulse", "DragPulse", @@ -398,6 +400,50 @@ def integration_weights_function(self) -> List[Tuple[Union[complex, float], int] } +@quam_dataclass +class WaveformPulse(Pulse): + """Pulse that uses a pre-defined waveform, as opposed to a function. + + For a single channel, only `waveform_I` is required. + For an IQ channel, both `waveform_I` and `waveform_Q` are required. + + The length of the pulse is derived from the length of `waveform_I`. + + Args: + waveform_I (list[float]): The in-phase waveform. + waveform_Q (list[float], optional): The quadrature waveform. + """ + + waveform_I: List[float] # pyright: ignore + waveform_Q: Optional[List[float]] = None + # Length is derived from the waveform_I length, but still needs to be declared + # to satisfy the dataclass, but we'll override its behavior + length: Optional[int] = None # pyright: ignore + + @property + def length(self): # noqa: 811 + if not isinstance(self.waveform_I, Iterable): + return None + return len(self.waveform_I) + + @length.setter + def length(self, length: Optional[int]): + if length is not None and not isinstance(length, property): + raise AttributeError(f"length is not writable with value {length}") + + def waveform_function(self): + if self.waveform_Q is None: + return np.array(self.waveform_I) + return np.array(self.waveform_I) + 1.0j * np.array(self.waveform_Q) + + def to_dict( + self, follow_references: bool = False, include_defaults: bool = False + ) -> Dict[str, Any]: + d = super().to_dict(follow_references, include_defaults) + d.pop("length") + return d + + @quam_dataclass class DragGaussianPulse(Pulse): """Gaussian-based DRAG pulse that compensate for the leakage and AC stark shift. diff --git a/tests/components/pulses/test_waveform_pulse.py b/tests/components/pulses/test_waveform_pulse.py new file mode 100644 index 00000000..a60d5522 --- /dev/null +++ b/tests/components/pulses/test_waveform_pulse.py @@ -0,0 +1,47 @@ +from collections.abc import Iterable +import numpy as np +import pytest +from quam.components.pulses import WaveformPulse + + +def test_waveform_pulse_length(): + pulse = WaveformPulse(waveform_I=[1, 2, 3]) + assert pulse.length == 3 + + pulse.waveform_I = [1, 2, 3, 4] + + with pytest.raises(AttributeError): + pulse.length = 5 + + assert pulse.length == 4 + + +def test_waveform_pulse_IQ(): + pulse = WaveformPulse(waveform_I=[1, 2, 3], waveform_Q=[4, 5, 6]) + assert np.all( + pulse.waveform_function() == np.array([1, 2, 3]) + 1.0j * np.array([4, 5, 6]) + ) + assert pulse.length + + +def test_waveform_pulse_IQ_mismatch(): + pulse = WaveformPulse(waveform_I=[1, 2, 3], waveform_Q=[4, 5]) + with pytest.raises(ValueError): + pulse.waveform_function() + + +def test_waveform_pulse_to_dict(): + pulse = WaveformPulse(waveform_I=[1, 2, 3], waveform_Q=[4, 5, 6]) + assert pulse.to_dict() == { + "waveform_I": [1, 2, 3], + "waveform_Q": [4, 5, 6], + } + + +def test_waveform_pulse_length_error(): + with pytest.raises(AttributeError): + pulse = WaveformPulse(waveform_I=[1, 2, 3], length=11) + + pulse = WaveformPulse(waveform_I=[1, 2, 3]) + with pytest.raises(AttributeError): + pulse.length = 11