From cacb9453c98edddfd79f604176e4bd4848e33d51 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 26 Jun 2024 14:48:16 +0200 Subject: [PATCH 01/31] Added relevant ports --- quam/components/ports.py | 98 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 quam/components/ports.py diff --git a/quam/components/ports.py b/quam/components/ports.py new file mode 100644 index 00000000..6b2bae37 --- /dev/null +++ b/quam/components/ports.py @@ -0,0 +1,98 @@ +from dataclasses import field +from typing import Dict, List, Literal, Optional, Tuple +from quam.core import QuamComponent, quam_dataclass + + +@quam_dataclass +class OPXPlusAnalogOutputPort(QuamComponent): + port: Tuple[str, int] + offset: float = 0.0 + delay: int = 0 + crosstalk: Dict[int, float] = field(default_factory=dict) + feedforward_filter: List[float] = field(default_factory=list) + feedback_filter: List[float] = field(default_factory=list) + shareable: bool = False + + +@quam_dataclass +class OPXPlusAnalogInputPort(QuamComponent): + port: Tuple[str, int] + offset: float = 0.0 + gain_db: int = 0 + shareable: bool = False + + +@quam_dataclass +class OPXPlusDigitalOutputPort(QuamComponent): + port: Tuple[str, int] + inverted: bool = False + shareable: bool = False + + +@quam_dataclass +class OPXPlusDigitalInputPort(QuamComponent): + port: Tuple[str, int] + deadtime: int = 4 + polarity: Literal["Rising", "Falling"] = "Rising" + threshold: float = 2.0 + shareable: bool = False + + +@quam_dataclass +class LFFEMAnalogOutputPort(QuamComponent): + port: Tuple[str, int] + offset: float = 0.0 + delay: int = 0 + crosstalk: Dict[int, float] = field(default_factory=dict) + feedforward_filter: List[float] = field(default_factory=list) + feedback_filter: List[float] = field(default_factory=list) + shareable: bool = False + sampling_rate: float = 1e9 # Either 1e9 or 2e9 + upsampling_mode: Literal["mw", "pulse"] = "mw" + output_mode: Literal["direct", "amplified"] = "direct" + + +@quam_dataclass +class LFFEMAnalogInputPort(QuamComponent): + port: Tuple[str, int] + offset: float = 0.0 + gain_db: int = 0 + shareable: bool = False + sampling_rate: float = 1e9 # Either 1e9 or 2e9 + + +@quam_dataclass +class LFFEMDigitalOutputPort(QuamComponent): + port: Tuple[str, int] + level: Literal["TTL", "LVTTL"] = "LVTTL" + inverted: bool = False + shareable: bool = False + + +@quam_dataclass +class MWFEMAnalogOutputPort(QuamComponent): + port: Tuple[str, int] + band: int + upconverter_frequency: Optional[float] = None + upconverters: Optional[Dict[int, float]] = None + delay: int = 0 + shareable: bool = False + sampling_rate: float = 1e9 # Either 1e9 or 2e9 + full_scale_power_dbm: int = -11 + + +@quam_dataclass +class MWFEMAnalogInputPort(QuamComponent): + port: Tuple[str, int] + band: int + downconverter_frequency: float + sampling_rate: float = 1e9 # Either 1e9 or 2e9 + shareable: bool = False + + +@quam_dataclass +class MWFEMDigitalOutputPort(QuamComponent): + port: Tuple[str, int] + level: Literal["TTL", "LVTTL"] = "LVTTL" + inverted: bool = False + shareable: bool = False From c8ad5bca25740d6cb103802229584bde8e00b2e4 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 26 Jun 2024 16:07:46 +0200 Subject: [PATCH 02/31] generalized ports --- quam/components/channels.py | 13 +++++++------ quam/components/ports.py | 35 ++++++++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/quam/components/channels.py b/quam/components/channels.py index 1a54fbe2..76cb4df8 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -4,6 +4,7 @@ from quam.components.hardware import BaseFrequencyConverter, Mixer, LocalOscillator from quam.components.pulses import Pulse, BaseReadoutPulse +from quam.components.ports import LFAnalogOutputPort, LFAnalogInputPort from quam.core import QuamComponent, quam_dataclass from quam.core.quam_classes import QuamDict from quam.utils import string_reference as str_ref @@ -485,7 +486,7 @@ class SingleChannel(Channel): is None. """ - opx_output: Union[Tuple[str, int], Tuple[str, int, int]] + opx_output: Union[Tuple[str, int], Tuple[str, int, int], LFAnalogOutputPort] filter_fir_taps: List[float] = None filter_iir_taps: List[float] = None @@ -560,7 +561,7 @@ class InSingleChannel(Channel): Used to account for signal smearing. """ - opx_input: Union[Tuple[str, int], Tuple[str, int, int]] + opx_input: Union[Tuple[str, int], Tuple[str, int, int], LFAnalogInputPort] opx_input_offset: float = None time_of_flight: int = 24 @@ -831,8 +832,8 @@ class IQChannel(Channel): for the IQ output. """ - opx_output_I: Union[Tuple[str, int], Tuple[str, int, int]] - opx_output_Q: Union[Tuple[str, int], Tuple[str, int, int]] + opx_output_I: Union[Tuple[str, int], Tuple[str, int, int], LFAnalogOutputPort] + opx_output_Q: Union[Tuple[str, int], Tuple[str, int, int], LFAnalogOutputPort] opx_output_offset_I: float = None opx_output_offset_Q: float = None @@ -1026,8 +1027,8 @@ class InIQChannel(Channel): input_gain (float): The gain of the input channel. Default is None. """ - opx_input_I: Union[Tuple[str, int], Tuple[str, int, int]] - opx_input_Q: Union[Tuple[str, int], Tuple[str, int, int]] + opx_input_I: Union[Tuple[str, int], Tuple[str, int, int], LFAnalogInputPort] + opx_input_Q: Union[Tuple[str, int], Tuple[str, int, int], LFAnalogInputPort] time_of_flight: int = 24 smearing: int = 0 diff --git a/quam/components/ports.py b/quam/components/ports.py index 6b2bae37..8be1a84c 100644 --- a/quam/components/ports.py +++ b/quam/components/ports.py @@ -1,10 +1,24 @@ from dataclasses import field -from typing import Dict, List, Literal, Optional, Tuple +from typing import ClassVar, Dict, List, Literal, Optional, Tuple from quam.core import QuamComponent, quam_dataclass @quam_dataclass -class OPXPlusAnalogOutputPort(QuamComponent): +class Port(QuamComponent): + port_type: ClassVar[str] + + +@quam_dataclass +class LFAnalogOutputPort(QuamComponent): + port_type: ClassVar[str] = "analog_output" + + +class LFAnalogInputPort(QuamComponent): + port_type: ClassVar[str] = "analog_input" + + +@quam_dataclass +class OPXPlusAnalogOutputPort(LFAnalogOutputPort): port: Tuple[str, int] offset: float = 0.0 delay: int = 0 @@ -13,9 +27,20 @@ class OPXPlusAnalogOutputPort(QuamComponent): feedback_filter: List[float] = field(default_factory=list) shareable: bool = False + def get_port_config(self): + return { + "port": self.port, + "offset": self.offset, + "delay": self.delay, + "crosstalk": self.crosstalk, + "feedforward_filter": self.feedforward_filter, + "feedback_filter": self.feedback_filter, + "shareable": self.shareable, + } + @quam_dataclass -class OPXPlusAnalogInputPort(QuamComponent): +class OPXPlusAnalogInputPort(LFAnalogInputPort): port: Tuple[str, int] offset: float = 0.0 gain_db: int = 0 @@ -39,7 +64,7 @@ class OPXPlusDigitalInputPort(QuamComponent): @quam_dataclass -class LFFEMAnalogOutputPort(QuamComponent): +class LFFEMAnalogOutputPort(LFAnalogOutputPort): port: Tuple[str, int] offset: float = 0.0 delay: int = 0 @@ -53,7 +78,7 @@ class LFFEMAnalogOutputPort(QuamComponent): @quam_dataclass -class LFFEMAnalogInputPort(QuamComponent): +class LFFEMAnalogInputPort(LFAnalogInputPort): port: Tuple[str, int] offset: float = 0.0 gain_db: int = 0 From 758edde76a4c3519d3d46a5fb0baa796d300a277 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 27 Jun 2024 12:31:03 +0200 Subject: [PATCH 03/31] cleanup of ports --- quam/components/ports.py | 217 ++++++++++++++++++++++++++++++--------- 1 file changed, 167 insertions(+), 50 deletions(-) diff --git a/quam/components/ports.py b/quam/components/ports.py index 8be1a84c..fdea6a14 100644 --- a/quam/components/ports.py +++ b/quam/components/ports.py @@ -1,25 +1,93 @@ +from abc import ABC, abstractmethod from dataclasses import field -from typing import ClassVar, Dict, List, Literal, Optional, Tuple +from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple from quam.core import QuamComponent, quam_dataclass +# ---- General ports ---- # @quam_dataclass -class Port(QuamComponent): +class Port(QuamComponent, ABC): port_type: ClassVar[str] + port: Union[Tuple[str, int], Tuple[str, int, int]] + @abstractmethod + def get_port_config( + self, config: Dict[str, Any], create: bool = True + ) -> Dict[str, Any]: + pass -@quam_dataclass -class LFAnalogOutputPort(QuamComponent): - port_type: ClassVar[str] = "analog_output" + @abstractmethod + def get_port_properties(self) -> Dict[str, Any]: + pass + def apply_to_config(self, config: Dict) -> None: + super().apply_to_config(config) -class LFAnalogInputPort(QuamComponent): - port_type: ClassVar[str] = "analog_input" + port_cfg = self.get_port_config(config) + port_properties = self.get_port_properties() + port_cfg.update(port_properties) @quam_dataclass -class OPXPlusAnalogOutputPort(LFAnalogOutputPort): +class OPXPlusPort(Port, ABC): port: Tuple[str, int] + + def get_port_config( + self, config: Dict[str, Any], create: bool = True + ) -> Dict[str, Any]: + controller_name, port = self.port + + if not create: + try: + return config["controllers"][controller_name][f"{self.port_type}"][port] + except KeyError: + raise KeyError( + f"Error generating config: controller {controller_name} does not " + f"have entry {self.port_type}s for port {self.port}" + ) + + controller_cfg = config["controllers"].setdefault(controller_name, {}) + ports_cfg = controller_cfg.setdefault(f"{self.port_type}s", {}) + port_cfg = ports_cfg.setdefault(port, {}) + return port_cfg + + +@quam_dataclass +class FEMPort(Port, ABC): + port: Tuple[str, int, int] + + def get_port_config( + self, config: Dict[str, Any], create: bool = True + ) -> Dict[str, Any]: + controller_name, fem, port = self.port + + if not create: + try: + fem_cfg = config["controllers"][controller_name]["fems"][fem] + except KeyError: + raise KeyError( + f"Error generating config: controller {controller_name} does not " + f"have entry for FEM {fem} for port {self.port}" + ) + try: + return fem_cfg[f"{self.port_type}s"][port] + except KeyError: + raise KeyError( + f"Error generating config: controller {controller_name} does not " + f"have entry {self.port_type}s for port {self.port}" + ) + + controller_cfg = config["controllers"].setdefault(controller_name, {}) + ports_cfg = controller_cfg.setdefault(f"{self.port_type}s", {}) + port_cfg = ports_cfg.setdefault(port, {}) + return port_cfg + + +# --- Analog ports --- # +@quam_dataclass +class LFAnalogOutputPort(QuamComponent, ABC): + port_type: ClassVar[str] = "analog_output" + offset: float = 0.0 delay: int = 0 crosstalk: Dict[int, float] = field(default_factory=dict) @@ -27,9 +95,8 @@ class OPXPlusAnalogOutputPort(LFAnalogOutputPort): feedback_filter: List[float] = field(default_factory=list) shareable: bool = False - def get_port_config(self): + def get_port_properties(self): return { - "port": self.port, "offset": self.offset, "delay": self.delay, "crosstalk": self.crosstalk, @@ -39,64 +106,59 @@ def get_port_config(self): } -@quam_dataclass -class OPXPlusAnalogInputPort(LFAnalogInputPort): - port: Tuple[str, int] +class LFAnalogInputPort(QuamComponent, ABC): + port_type: ClassVar[str] = "analog_input" + offset: float = 0.0 gain_db: int = 0 shareable: bool = False + def get_port_properties(self): + return { + "offset": self.offset, + "gain_db": self.gain_db, + "shareable": self.shareable, + } + @quam_dataclass -class OPXPlusDigitalOutputPort(QuamComponent): - port: Tuple[str, int] - inverted: bool = False - shareable: bool = False +class OPXPlusAnalogOutputPort(LFAnalogOutputPort, OPXPlusPort): + pass @quam_dataclass -class OPXPlusDigitalInputPort(QuamComponent): - port: Tuple[str, int] - deadtime: int = 4 - polarity: Literal["Rising", "Falling"] = "Rising" - threshold: float = 2.0 - shareable: bool = False +class OPXPlusAnalogInputPort(LFAnalogInputPort, OPXPlusPort): + pass @quam_dataclass -class LFFEMAnalogOutputPort(LFAnalogOutputPort): - port: Tuple[str, int] - offset: float = 0.0 - delay: int = 0 - crosstalk: Dict[int, float] = field(default_factory=dict) - feedforward_filter: List[float] = field(default_factory=list) - feedback_filter: List[float] = field(default_factory=list) - shareable: bool = False +class LFFEMAnalogOutputPort(LFAnalogOutputPort, FEMPort): sampling_rate: float = 1e9 # Either 1e9 or 2e9 upsampling_mode: Literal["mw", "pulse"] = "mw" output_mode: Literal["direct", "amplified"] = "direct" + def get_port_properties(self) -> Dict[str, Any]: + port_properties = super().get_port_properties() + port_properties["sampling_rate"] = self.sampling_rate + port_properties["upsampling_mode"] = self.upsampling_mode + port_properties["output_mode"] = self.output_mode + return port_properties + @quam_dataclass -class LFFEMAnalogInputPort(LFAnalogInputPort): - port: Tuple[str, int] - offset: float = 0.0 - gain_db: int = 0 - shareable: bool = False +class LFFEMAnalogInputPort(LFAnalogInputPort, FEMPort): sampling_rate: float = 1e9 # Either 1e9 or 2e9 - -@quam_dataclass -class LFFEMDigitalOutputPort(QuamComponent): - port: Tuple[str, int] - level: Literal["TTL", "LVTTL"] = "LVTTL" - inverted: bool = False - shareable: bool = False + def get_port_properties(self) -> Dict[str, Any]: + port_properties = super().get_port_properties() + port_properties["sampling_rate"] = self.sampling_rate + return port_properties @quam_dataclass -class MWFEMAnalogOutputPort(QuamComponent): - port: Tuple[str, int] +class MWFEMAnalogOutputPort(FEMPort): + port_type: ClassVar[str] = "analog_output" + band: int upconverter_frequency: Optional[float] = None upconverters: Optional[Dict[int, float]] = None @@ -105,19 +167,74 @@ class MWFEMAnalogOutputPort(QuamComponent): sampling_rate: float = 1e9 # Either 1e9 or 2e9 full_scale_power_dbm: int = -11 + def get_port_properties(self) -> Dict[str, Any]: + return { + "band": self.band, + "upconverter_frequency": self.upconverter_frequency, + "upconverters": self.upconverters, + "delay": self.delay, + "shareable": self.shareable, + "sampling_rate": self.sampling_rate, + "full_scale_power_dbm": self.full_scale_power_dbm, + } + @quam_dataclass -class MWFEMAnalogInputPort(QuamComponent): - port: Tuple[str, int] +class MWFEMAnalogInputPort(FEMPort): + port_type: ClassVar[str] = "analog_input" + band: int downconverter_frequency: float sampling_rate: float = 1e9 # Either 1e9 or 2e9 shareable: bool = False + def get_port_properties(self) -> Dict[str, Any]: + return { + "band": self.band, + "downconverter_frequency": self.downconverter_frequency, + "sampling_rate": self.sampling_rate, + "shareable": self.shareable, + } + +# --- Digital ports --- # @quam_dataclass -class MWFEMDigitalOutputPort(QuamComponent): - port: Tuple[str, int] - level: Literal["TTL", "LVTTL"] = "LVTTL" +class DigitalOutputPort(QuamComponent, ABC): + port_type: ClassVar[str] = "digital_output" + inverted: bool = False shareable: bool = False + + def get_port_properties(self) -> Dict[str, Any]: + return {"inverted": self.inverted, "shareable": self.shareable} + + +@quam_dataclass +class OPXPlusDigitalOutputPort(DigitalOutputPort, OPXPlusPort): + pass + + +@quam_dataclass +class OPXPlusDigitalInputPort(OPXPlusPort): + deadtime: int = 4 + polarity: Literal["Rising", "Falling"] = "Rising" + threshold: float = 2.0 + shareable: bool = False + + def get_port_properties(self) -> Dict[str, Any]: + return { + "deadtime": self.deadtime, + "polarity": self.polarity, + "threshold": self.threshold, + "shareable": self.shareable, + } + + +@quam_dataclass +class FEMDigitalOutputPort(DigitalOutputPort, FEMPort): + level: Literal["TTL", "LVTTL"] = "LVTTL" + + def get_port_properties(self) -> Dict[str, Any]: + port_properties = super().get_port_properties() + port_properties["level"] = self.level + return port_properties From 2cb0e4066bb28f62ae9b1c0a210366186220a8b6 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 28 Jun 2024 12:29:21 +0200 Subject: [PATCH 04/31] fix: missing import --- quam/components/ports.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quam/components/ports.py b/quam/components/ports.py index fdea6a14..2c6d6def 100644 --- a/quam/components/ports.py +++ b/quam/components/ports.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from dataclasses import field -from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple +from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple, Union from quam.core import QuamComponent, quam_dataclass @@ -106,6 +106,7 @@ def get_port_properties(self): } +@quam_dataclass class LFAnalogInputPort(QuamComponent, ABC): port_type: ClassVar[str] = "analog_input" From 3f31928d7a005b25412f77c9ab40f958a3d2fafd Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 28 Jun 2024 12:29:37 +0200 Subject: [PATCH 05/31] adding ports to channels --- quam/components/channels.py | 118 +++++++++++++++++++++--------------- 1 file changed, 68 insertions(+), 50 deletions(-) diff --git a/quam/components/channels.py b/quam/components/channels.py index 76cb4df8..2ae34f10 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -4,7 +4,13 @@ from quam.components.hardware import BaseFrequencyConverter, Mixer, LocalOscillator from quam.components.pulses import Pulse, BaseReadoutPulse -from quam.components.ports import LFAnalogOutputPort, LFAnalogInputPort +from quam.components.ports import ( + DigitalOutputPort, + FEMDigitalOutputPort, + LFAnalogOutputPort, + LFAnalogInputPort, + OPXPlusDigitalOutputPort, +) from quam.core import QuamComponent, quam_dataclass from quam.core.quam_classes import QuamDict from quam.utils import string_reference as str_ref @@ -70,7 +76,7 @@ class DigitalOutputChannel(QuamComponent): Default is False. .""" - opx_output: Union[Tuple[str, int], Tuple[str, int, int]] + opx_output: Union[Tuple[str, int], Tuple[str, int, int], DigitalOutputPort] delay: int = None buffer: int = None @@ -87,7 +93,12 @@ def generate_element_config(self) -> Dict[str, int]: Dict[str, int]: The digital channel config entry. Contains "port", and optionally "delay", "buffer" if specified """ - digital_cfg = {"port": tuple(self.opx_output)} + if isinstance(self.opx_output, DigitalOutputPort): + opx_output = self.opx_output.port + else: + opx_output = self.opx_output + + digital_cfg: Dict[str, Any] = {"port": tuple(opx_output)} if self.delay is not None: digital_cfg["delay"] = self.delay if self.buffer is not None: @@ -103,33 +114,28 @@ def apply_to_config(self, config: dict) -> None: See [`QuamComponent.apply_to_config`][quam.core.quam_classes.QuamComponent.apply_to_config] for details. """ - if len(self.opx_output) == 2: - controller_name, port = self.opx_output - controller_cfg = config["controllers"].setdefault(controller_name, {}) - controller_cfg.setdefault("digital_outputs", {}) - port_cfg = controller_cfg["digital_outputs"].setdefault(port, {}) - else: - controller_name, fem, port = self.opx_output - controller_cfg = config["controllers"].setdefault(controller_name, {}) - controller_cfg.setdefault("fems", {}) - fem_cfg = controller_cfg["fems"].setdefault(fem, {"type": "LF"}) - fem_cfg.setdefault("digital_outputs", {}) - port_cfg = fem_cfg["digital_outputs"].setdefault(port, {}) - - if self.shareable is not None: - if port_cfg.get("shareable", self.shareable) != self.shareable: - raise ValueError( - f"Channel {self.name} has conflicting shareable settings: " - f"{port_cfg['shareable']} and {self.shareable}" + if isinstance(self.opx_output, DigitalOutputPort): + if self.shareable is not None: + warnings.warn( + f"Property {self.name}.shareable (={self.shareable}) is ignored " + "because it should be set in {self.name}.opx_output.shareable" ) - port_cfg["shareable"] = self.shareable - if self.inverted is not None: - if port_cfg.get("inverted", self.inverted) != self.inverted: - raise ValueError( - f"Channel {self.name} has conflicting inverted settings: " - f"{port_cfg['inverted']} and {self.inverted}" + if self.inverted is not None: + warnings.warn( + f"Property {self.name}.inverted (={self.inverted}) is ignored " + "because it should be set in {self.name}.opx_output.inverted" ) - port_cfg["inverted"] = self.inverted + return + + if len(self.opx_output) == 2: + digital_output_port = OPXPlusDigitalOutputPort( + port=self.opx_output, shareable=self.shareable, inverted=self.inverted + ) + else: + digital_output_port = FEMDigitalOutputPort( + port=self.opx_output, shareable=self.shareable, inverted=self.inverted + ) + digital_output_port.apply_to_config(config) @quam_dataclass @@ -395,7 +401,7 @@ def frame_rotation_2pi(self, angle: QuaNumberType): """ frame_rotation_2pi(angle, self.name) - def _add_analog_port_to_config( + def _config_add_analog_port( self, address: Union[Tuple[str, int], Tuple[str, int, int]], config, @@ -523,22 +529,25 @@ def apply_to_config(self, config: dict): ) element_config = config["elements"][self.name] - element_config["singleInput"] = {"port": tuple(self.opx_output)} if self.intermediate_frequency is not None: element_config["intermediate_frequency"] = self.intermediate_frequency - port_config = self._add_analog_port_to_config( - self.opx_output, config, self.opx_output_offset, "output" - ) + if isinstance(self.opx_output, LFAnalogOutputPort): + element_config["singleInput"] = {"port": tuple(self.opx_output.port)} + else: + element_config["singleInput"] = {"port": tuple(self.opx_output)} + port_config = self._config_add_analog_port( + self.opx_output, config, self.opx_output_offset, "output" + ) - if self.filter_fir_taps is not None: - output_filter = port_config.setdefault("filter", {}) - output_filter["feedforward"] = list(self.filter_fir_taps) + if self.filter_fir_taps is not None: + output_filter = port_config.setdefault("filter", {}) + output_filter["feedforward"] = list(self.filter_fir_taps) - if self.filter_iir_taps is not None: - output_filter = port_config.setdefault("filter", {}) - output_filter["feedback"] = list(self.filter_iir_taps) + if self.filter_iir_taps is not None: + output_filter = port_config.setdefault("filter", {}) + output_filter["feedback"] = list(self.filter_iir_taps) @quam_dataclass @@ -577,13 +586,16 @@ def apply_to_config(self, config: dict): super().apply_to_config(config) # Note outputs instead of inputs because it's w.r.t. the QPU - config["elements"][self.name]["outputs"] = {"out1": tuple(self.opx_input)} - config["elements"][self.name]["smearing"] = self.smearing - config["elements"][self.name]["time_of_flight"] = self.time_of_flight + element_config = config["elements"][self.name] + element_config["smearing"] = self.smearing + element_config["time_of_flight"] = self.time_of_flight - self._add_analog_port_to_config( - self.opx_input, config, self.opx_input_offset, "input" - ) + if isinstance(self.opx_input, LFAnalogInputPort): + element_config["outputs"] = {"out1": tuple(self.opx_input.port)} + else: + self._config_add_analog_port( + self.opx_input, config, self.opx_input_offset, "input" + ) def measure( self, @@ -955,7 +967,12 @@ def apply_to_config(self, config: dict): """ # Add pulses & waveforms super().apply_to_config(config) - opx_outputs = {"I": tuple(self.opx_output_I), "Q": tuple(self.opx_output_Q)} + + opx_outputs = {"I": self.opx_output_I, "Q": self.opx_output_Q} + opx_ports = { + key: tuple(val.port if isinstance(val, LFAnalogOutputPort) else val) + for key, val in opx_outputs.items() + } offsets = {"I": self.opx_output_offset_I, "Q": self.opx_output_offset_Q} if str_ref.is_reference(self.name): @@ -989,7 +1006,7 @@ def apply_to_config(self, config: dict): f"reference: {self.frequency_converter_up}" ) else: - element_cfg["mixInputs"] = {**opx_outputs} + element_cfg["mixInputs"] = {**opx_ports} if self.mixer is not None: element_cfg["mixInputs"]["mixer"] = self.mixer.name if self.local_oscillator is not None: @@ -998,9 +1015,10 @@ def apply_to_config(self, config: dict): ] = self.local_oscillator.frequency for I_or_Q in ["I", "Q"]: - port_output = opx_outputs[I_or_Q] + opx_output = opx_outputs[I_or_Q] offset = offsets[I_or_Q] - self._add_analog_port_to_config(port_output, config, offset, "output") + if not isinstance(opx_output, LFAnalogOutputPort): + self._config_add_analog_port(opx_output, config, offset, "output") @quam_dataclass @@ -1086,7 +1104,7 @@ def apply_to_config(self, config: dict): for I_or_Q in ["I", "Q"]: curr_input = opx_inputs[I_or_Q] offset = offsets[I_or_Q] - port_config = self._add_analog_port_to_config( + port_config = self._config_add_analog_port( curr_input, config, offset, port_type="input" ) From 9c6986d749664f3050e742685d96b35355f4dcd3 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 28 Jun 2024 13:04:29 +0200 Subject: [PATCH 06/31] always create Port when adding in channels --- quam/components/channels.py | 179 +++++++++++++----------------------- 1 file changed, 62 insertions(+), 117 deletions(-) diff --git a/quam/components/channels.py b/quam/components/channels.py index 2ae34f10..af29ad08 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -9,6 +9,10 @@ FEMDigitalOutputPort, LFAnalogOutputPort, LFAnalogInputPort, + LFFEMAnalogInputPort, + LFFEMAnalogOutputPort, + OPXPlusAnalogInputPort, + OPXPlusAnalogOutputPort, OPXPlusDigitalOutputPort, ) from quam.core import QuamComponent, quam_dataclass @@ -401,35 +405,6 @@ def frame_rotation_2pi(self, angle: QuaNumberType): """ frame_rotation_2pi(angle, self.name) - def _config_add_analog_port( - self, - address: Union[Tuple[str, int], Tuple[str, int, int]], - config, - offset: float, - port_type: Literal["input", "output"], - ) -> Dict[str, Any]: - if len(address) == 2: - controller_name, port = address - controller_cfg = _config_add_opx_controller(config, controller_name) - else: - controller_name, fem, port = address - controller_cfg = _config_add_opx1000_controller( - config, controller_name, fem - ) - - port_config = controller_cfg[f"analog_{port_type}s"].setdefault(port, {}) - # If no offset specified, it will be added at the end of config generation - if offset is not None: - if abs(port_config.get("offset", offset) - offset) > 1e-4: - warnings.warn( - f"Channel {self.name} has conflicting {port_type} offsets: " - f"{port_config['offset']} and {offset}. Multiple channel " - f"elements are trying to set different offsets to port {port}", - UserWarning, - ) - port_config["offset"] = offset - return port_config - def _config_add_digital_outputs(self, config: Dict[str, dict]) -> None: """Adds the digital outputs to the QUA config. @@ -534,20 +509,25 @@ def apply_to_config(self, config: dict): element_config["intermediate_frequency"] = self.intermediate_frequency if isinstance(self.opx_output, LFAnalogOutputPort): - element_config["singleInput"] = {"port": tuple(self.opx_output.port)} + opx_port = self.opx_output + elif len(self.opx_output) == 2: + opx_port = OPXPlusAnalogOutputPort( + port=self.opx_output, + offset=self.opx_output_offset, + feedforward_filter=list(self.filter_fir_taps), + feedback_filter=list(self.filter_iir_taps), + ) + opx_port.apply_to_config(config) else: - element_config["singleInput"] = {"port": tuple(self.opx_output)} - port_config = self._config_add_analog_port( - self.opx_output, config, self.opx_output_offset, "output" + opx_port = LFFEMAnalogOutputPort( + port=self.opx_output, + offset=self.opx_output_offset, + feedforward_filter=list(self.filter_fir_taps), + feedback_filter=list(self.filter_iir_taps), ) + opx_port.apply_to_config(config) - if self.filter_fir_taps is not None: - output_filter = port_config.setdefault("filter", {}) - output_filter["feedforward"] = list(self.filter_fir_taps) - - if self.filter_iir_taps is not None: - output_filter = port_config.setdefault("filter", {}) - output_filter["feedback"] = list(self.filter_iir_taps) + element_config["singleInput"] = {"port": tuple(opx_port.port)} @quam_dataclass @@ -591,11 +571,19 @@ def apply_to_config(self, config: dict): element_config["time_of_flight"] = self.time_of_flight if isinstance(self.opx_input, LFAnalogInputPort): - element_config["outputs"] = {"out1": tuple(self.opx_input.port)} + opx_port = self.opx_input + elif len(self.opx_input) == 2: + opx_port = OPXPlusAnalogInputPort( + port=self.opx_input, offset=self.opx_input_offset + ) + opx_port.apply_to_config(config) else: - self._config_add_analog_port( - self.opx_input, config, self.opx_input_offset, "input" + opx_port = LFFEMAnalogInputPort( + port=self.opx_input, offset=self.opx_input_offset ) + opx_port.apply_to_config(config) + + element_config["outputs"] = {"out1": tuple(opx_port.port)} def measure( self, @@ -969,11 +957,6 @@ def apply_to_config(self, config: dict): super().apply_to_config(config) opx_outputs = {"I": self.opx_output_I, "Q": self.opx_output_Q} - opx_ports = { - key: tuple(val.port if isinstance(val, LFAnalogOutputPort) else val) - for key, val in opx_outputs.items() - } - offsets = {"I": self.opx_output_offset_I, "Q": self.opx_output_offset_Q} if str_ref.is_reference(self.name): raise AttributeError( @@ -1014,11 +997,17 @@ def apply_to_config(self, config: dict): "lo_frequency" ] = self.local_oscillator.frequency - for I_or_Q in ["I", "Q"]: - opx_output = opx_outputs[I_or_Q] - offset = offsets[I_or_Q] - if not isinstance(opx_output, LFAnalogOutputPort): - self._config_add_analog_port(opx_output, config, offset, "output") + opx_outputs = [self.opx_output_I, self.opx_output_Q] + offsets = [self.opx_output_offset_I, self.opx_output_offset_Q] + for opx_output, offset in zip(opx_outputs, offsets): + if isinstance(opx_output, LFAnalogOutputPort): + opx_port = opx_output + elif len(opx_output) == 2: + opx_port = OPXPlusAnalogOutputPort(port=opx_output, offset=offset) + opx_port.apply_to_config(config) + else: + opx_port = LFFEMAnalogOutputPort(port=opx_output, offset=offset) + opx_port.apply_to_config(config) @quam_dataclass @@ -1068,9 +1057,6 @@ def apply_to_config(self, config: dict): """ super().apply_to_config(config) - opx_inputs = {"I": tuple(self.opx_input_I), "Q": tuple(self.opx_input_Q)} - offsets = {"I": self.opx_input_offset_I, "Q": self.opx_input_offset_Q} - # Note outputs instead of inputs because it's w.r.t. the QPU element_cfg = config["elements"][self.name] element_cfg["smearing"] = self.smearing @@ -1096,20 +1082,26 @@ def apply_to_config(self, config: dict): f"reference: {self.frequency_converter_down}" ) else: - element_cfg["outputs"] = { - "out1": tuple(self.opx_input_I), - "out2": tuple(self.opx_input_Q), - } - - for I_or_Q in ["I", "Q"]: - curr_input = opx_inputs[I_or_Q] - offset = offsets[I_or_Q] - port_config = self._config_add_analog_port( - curr_input, config, offset, port_type="input" - ) - - if self.input_gain is not None: - port_config["gain_db"] = self.input_gain + # To be filled in next section + element_cfg["outputs"] = {} + + opx_inputs = [self.opx_input_I, self.opx_input_Q] + offsets = [self.opx_input_offset_I, self.opx_input_offset_Q] + for k, (opx_input, offset) in enumerate(zip(opx_inputs, offsets), start=1): + if isinstance(opx_input, LFAnalogInputPort): + opx_port = opx_input + elif len(opx_input) == 2: + opx_port = OPXPlusAnalogInputPort( + port=opx_input, offset=offset, gain_db=self.input_gain + ) + opx_port.apply_to_config(config) + else: + opx_port = LFFEMAnalogInputPort( + port=opx_input, offset=offset, gain_db=self.input_gain + ) + opx_port.apply_to_config(config) + if not isinstance(self.frequency_converter_down, OctaveDownConverter): + element_cfg["outputs"][f"out{k}"] = tuple(opx_port.port) def measure( self, @@ -1485,50 +1477,3 @@ class InIQOutSingleChannel(SingleChannel, InIQChannel): """ pass - - -def _config_add_opx_controller( - config: Dict[str, dict], controller_name: str -) -> Dict[str, dict]: - """Adds a controller to the config if it doesn't exist, and returns its config. - - config.controllers. will be created if it doesn't exist. - It will also add the analog_outputs, digital_outputs, and analog_inputs keys - - Args: - config (dict): The QUA config that's in the process of being generated. - controller_name (str): The name of the controller. - - Returns: - Dict[str, dict]: The config entry for the controller. - """ - config["controllers"].setdefault(controller_name, {}) - controller_cfg = config["controllers"][controller_name] - for key in ["analog_outputs", "digital_outputs", "analog_inputs"]: - controller_cfg.setdefault(key, {}) - - return controller_cfg - - -def _config_add_opx1000_controller( - config: Dict[str, dict], controller_name: str, fem_idx: int -) -> Dict[str, dict]: - """Adds a controller to the config if it doesn't exist, and returns its config. - - config.controllers. will be created if it doesn't exist. - It will also add the analog_outputs, digital_outputs, and analog_inputs keys - - Args: - config (dict): The QUA config that's in the process of being generated. - controller_name (str): The name of the controller. - - Returns: - Dict[str, dict]: The config entry for the controller. - """ - controller_cfg = config["controllers"].setdefault(controller_name, {}) - fems_config = controller_cfg.setdefault("fems", {}) - fem_config = fems_config.setdefault(fem_idx, {"type": "LF"}) - for key in ["analog_outputs", "digital_outputs", "analog_inputs"]: - fem_config.setdefault(key, {}) - - return fem_config From 5fc369c6149e3e591e538effaa5c3517e70cdc27 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 28 Jun 2024 14:20:53 +0200 Subject: [PATCH 07/31] Add support for MW FEM --- quam/components/channels.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/quam/components/channels.py b/quam/components/channels.py index af29ad08..31086ee5 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -11,6 +11,8 @@ LFAnalogInputPort, LFFEMAnalogInputPort, LFFEMAnalogOutputPort, + MWFEMAnalogInputPort, + MWFEMAnalogOutputPort, OPXPlusAnalogInputPort, OPXPlusAnalogOutputPort, OPXPlusDigitalOutputPort, @@ -1477,3 +1479,32 @@ class InIQOutSingleChannel(SingleChannel, InIQChannel): """ pass + + +@quam_dataclass +class MWChannel(QuamComponent): + opx_output: MWFEMAnalogOutputPort + upconverter: int = 1 + + def apply_to_config(self, config: Dict) -> None: + super().apply_to_config(config) + + element_cfg = config["elements"][self.name] + element_cfg["MWInput"] = tuple(self.opx_output.port) + element_cfg["upconverter"] = self.upconverter + + +@quam_dataclass +class InMWChannel(QuamComponent): + opx_input: MWFEMAnalogInputPort + + def apply_to_config(self, config: Dict) -> None: + super().apply_to_config(config) + + element_cfg = config["elements"][self.name] + element_cfg["MWOutput"] = tuple(self.opx_input.port) + + +@quam_dataclass +class InOutMWChannel(MWChannel, InMWChannel): + pass From 1b6bed434bc4c4effc7e84faa01ad5effaf81a21 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 28 Jun 2024 14:41:14 +0200 Subject: [PATCH 08/31] add overwriting warning to ports --- quam/components/ports.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/quam/components/ports.py b/quam/components/ports.py index 2c6d6def..cd09c168 100644 --- a/quam/components/ports.py +++ b/quam/components/ports.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod from dataclasses import field from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple, Union +import warnings from quam.core import QuamComponent, quam_dataclass @@ -20,12 +21,27 @@ def get_port_config( def get_port_properties(self) -> Dict[str, Any]: pass + @staticmethod + def _update_port_config(port_config, port_properties): + for key, value in port_properties.items(): + try: + if key in port_config and value != port_config[key]: + warnings.warn( + f"Error generating QUA config: Controller {self.port_type} " + f"port {self.port} already has entry for {key}. This likely " + f"means that the port is being configured multiple times. " + f"Overwriting {port_config['key']} → {value}." + ) + except Exception: + pass + port_config[key] = value + def apply_to_config(self, config: Dict) -> None: super().apply_to_config(config) port_cfg = self.get_port_config(config) port_properties = self.get_port_properties() - port_cfg.update(port_properties) + self._update_port_config(port_cfg, port_properties) @quam_dataclass From 2166e6327d1cea589e7cb8cd91b3c698e6be6e58 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 28 Jun 2024 15:03:16 +0200 Subject: [PATCH 09/31] added tests for ports --- quam/components/ports.py | 17 ++- tests/components/ports/test_digital_ports.py | 117 ++++++++++++++++++ .../ports/test_lf_fem_analog_ports.py | 102 +++++++++++++++ .../ports/test_mw_fem_analog_ports.py | 112 +++++++++++++++++ tests/components/ports/test_opx_ports.py | 85 +++++++++++++ 5 files changed, 428 insertions(+), 5 deletions(-) create mode 100644 tests/components/ports/test_digital_ports.py create mode 100644 tests/components/ports/test_lf_fem_analog_ports.py create mode 100644 tests/components/ports/test_mw_fem_analog_ports.py create mode 100644 tests/components/ports/test_opx_ports.py diff --git a/quam/components/ports.py b/quam/components/ports.py index cd09c168..6d16de61 100644 --- a/quam/components/ports.py +++ b/quam/components/ports.py @@ -94,7 +94,9 @@ def get_port_config( ) controller_cfg = config["controllers"].setdefault(controller_name, {}) - ports_cfg = controller_cfg.setdefault(f"{self.port_type}s", {}) + fems_cfg = controller_cfg.setdefault("fems", {}) + fem_cfg = fems_cfg.setdefault(fem, {}) + ports_cfg = fem_cfg.setdefault(f"{self.port_type}s", {}) port_cfg = ports_cfg.setdefault(port, {}) return port_cfg @@ -185,15 +187,18 @@ class MWFEMAnalogOutputPort(FEMPort): full_scale_power_dbm: int = -11 def get_port_properties(self) -> Dict[str, Any]: - return { + port_cfg = { "band": self.band, - "upconverter_frequency": self.upconverter_frequency, - "upconverters": self.upconverters, "delay": self.delay, "shareable": self.shareable, "sampling_rate": self.sampling_rate, "full_scale_power_dbm": self.full_scale_power_dbm, } + if self.upconverter_frequency is not None: + port_cfg["upconverter_frequency"] = self.upconverter_frequency + if self.upconverters is not None: + port_cfg["upconverters"] = self.upconverters + return port_cfg @quam_dataclass @@ -233,8 +238,10 @@ class OPXPlusDigitalOutputPort(DigitalOutputPort, OPXPlusPort): @quam_dataclass class OPXPlusDigitalInputPort(OPXPlusPort): + port_type: ClassVar[str] = "digital_input" + deadtime: int = 4 - polarity: Literal["Rising", "Falling"] = "Rising" + polarity: Literal["rising", "falling"] = "rising" threshold: float = 2.0 shareable: bool = False diff --git a/tests/components/ports/test_digital_ports.py b/tests/components/ports/test_digital_ports.py new file mode 100644 index 00000000..fb739da7 --- /dev/null +++ b/tests/components/ports/test_digital_ports.py @@ -0,0 +1,117 @@ +import pytest + +from quam.components.ports import ( + FEMDigitalOutputPort, + OPXPlusDigitalInputPort, + OPXPlusDigitalOutputPort, +) + + +def test_opx_plus_digital_output_port(): + with pytest.raises(TypeError): + OPXPlusDigitalOutputPort() + + port = OPXPlusDigitalOutputPort(port=("con1", 2)) + assert port.port == ("con1", 2) + assert port.port_type == "digital_output" + assert port.inverted == False + assert port.shareable == False + + assert port.get_port_properties() == { + "inverted": False, + "shareable": False, + } + + cfg = {"controllers": {}} + port.apply_to_config(cfg) + + assert cfg == { + "controllers": { + "con1": { + "digital_outputs": { + 2: { + "inverted": False, + "shareable": False, + } + } + } + } + } + + +def test_opx_plus_digital_input_port(): + with pytest.raises(TypeError): + OPXPlusDigitalInputPort() + + port = OPXPlusDigitalInputPort(port=("con1", 2)) + assert port.port == ("con1", 2) + assert port.port_type == "digital_input" + assert port.deadtime == 4 + assert port.polarity == "rising" + assert port.threshold == 2.0 + assert port.shareable == False + + assert port.get_port_properties() == { + "deadtime": 4, + "polarity": "rising", + "threshold": 2.0, + "shareable": False, + } + + cfg = {"controllers": {}} + port.apply_to_config(cfg) + + assert cfg == { + "controllers": { + "con1": { + "digital_inputs": { + 2: { + "deadtime": 4, + "polarity": "rising", + "threshold": 2.0, + "shareable": False, + } + } + } + } + } + + +def test_fem_digital_output_port(): + with pytest.raises(TypeError): + FEMDigitalOutputPort() + + port = FEMDigitalOutputPort(port=("con1", 1, 2)) + + assert port.port == ("con1", 1, 2) + assert port.port_type == "digital_output" + assert port.inverted == False + assert port.shareable == False + assert port.level == "LVTTL" + + assert port.get_port_properties() == { + "inverted": False, + "shareable": False, + "level": "LVTTL", + } + + cfg = {"controllers": {}} + port.apply_to_config(cfg) + + assert cfg == { + "controllers": { + "con1": { + "fems": { + 1: { + "digital_outputs": { + 2: { + "inverted": False, + "shareable": False, + "level": "LVTTL", + } + } + } + } + } + } + } diff --git a/tests/components/ports/test_lf_fem_analog_ports.py b/tests/components/ports/test_lf_fem_analog_ports.py new file mode 100644 index 00000000..fb5ef491 --- /dev/null +++ b/tests/components/ports/test_lf_fem_analog_ports.py @@ -0,0 +1,102 @@ +import pytest + +from quam.components.ports import LFFEMAnalogInputPort, LFFEMAnalogOutputPort + + +def test_lf_fem_analog_output_port(): + with pytest.raises(TypeError): + LFFEMAnalogOutputPort() + + port = LFFEMAnalogOutputPort(port=("con1", 1, 2)) + assert port.port == ("con1", 1, 2) + assert port.port_type == "analog_output" + assert port.offset == 0.0 + assert port.delay == 0 + assert port.crosstalk == {} + assert port.feedforward_filter == [] + assert port.feedback_filter == [] + assert port.shareable == False + assert port.output_mode == "direct" + assert port.sampling_rate == 1e9 + assert port.upsampling_mode == "mw" + + assert port.get_port_properties() == { + "offset": 0.0, + "delay": 0, + "crosstalk": {}, + "feedforward_filter": [], + "feedback_filter": [], + "shareable": False, + "output_mode": "direct", + "sampling_rate": 1e9, + "upsampling_mode": "mw", + } + + cfg = {"controllers": {}} + port.apply_to_config(cfg) + + assert cfg == { + "controllers": { + "con1": { + "fems": { + 1: { + "analog_outputs": { + 2: { + "offset": 0.0, + "delay": 0, + "crosstalk": {}, + "feedforward_filter": [], + "feedback_filter": [], + "shareable": False, + "output_mode": "direct", + "sampling_rate": 1e9, + "upsampling_mode": "mw", + } + } + } + } + } + } + } + + +def test_lf_fem_analog_input_port(): + with pytest.raises(TypeError): + LFFEMAnalogInputPort() + + port = LFFEMAnalogInputPort(port=("con1", 1, 2)) + assert port.port == ("con1", 1, 2) + assert port.port_type == "analog_input" + assert port.offset == 0.0 + assert port.gain_db == 0 + assert port.shareable == False + assert port.sampling_rate == 1e9 + + assert port.get_port_properties() == { + "offset": 0.0, + "gain_db": 0, + "shareable": False, + "sampling_rate": 1e9, + } + + cfg = {"controllers": {}} + port.apply_to_config(cfg) + + assert cfg == { + "controllers": { + "con1": { + "fems": { + 1: { + "analog_inputs": { + 2: { + "offset": 0.0, + "gain_db": 0, + "shareable": False, + "sampling_rate": 1e9, + } + } + } + } + } + } + } diff --git a/tests/components/ports/test_mw_fem_analog_ports.py b/tests/components/ports/test_mw_fem_analog_ports.py new file mode 100644 index 00000000..43382a9a --- /dev/null +++ b/tests/components/ports/test_mw_fem_analog_ports.py @@ -0,0 +1,112 @@ +import pytest + +from quam.components.ports import MWFEMAnalogInputPort, MWFEMAnalogOutputPort + + +def test_mw_fem_analog_output_port(): + with pytest.raises(TypeError): + MWFEMAnalogOutputPort() + + with pytest.raises(TypeError): + port = MWFEMAnalogOutputPort(port=("con1", 1, 2)) + + port = MWFEMAnalogOutputPort(port=("con1", 1, 2), band=1) + assert port.port == ("con1", 1, 2) + assert port.port_type == "analog_output" + assert port.band == 1 + assert port.upconverter_frequency is None + assert port.upconverters is None + assert port.delay == 0 + assert port.shareable == False + assert port.sampling_rate == 1e9 + assert port.full_scale_power_dbm == -11 + + assert port.get_port_properties() == { + "band": 1, + "delay": 0, + "shareable": False, + "sampling_rate": 1e9, + "full_scale_power_dbm": -11, + } + + port.upconverter_frequency = 5e9 + assert port.get_port_properties() == { + "band": 1, + "upconverter_frequency": 5e9, + "delay": 0, + "shareable": False, + "sampling_rate": 1e9, + "full_scale_power_dbm": -11, + } + + cfg = {"controllers": {}} + port.apply_to_config(cfg) + + assert cfg == { + "controllers": { + "con1": { + "fems": { + 1: { + "analog_outputs": { + 2: { + "band": 1, + "upconverter_frequency": 5e9, + "delay": 0, + "shareable": False, + "sampling_rate": 1e9, + "full_scale_power_dbm": -11, + } + } + } + } + } + } + } + + +def test_mw_fem_analog_input_ports(): + with pytest.raises(TypeError): + MWFEMAnalogInputPort() + + with pytest.raises(TypeError): + port = MWFEMAnalogInputPort(port=("con1", 1, 2)) + + port = MWFEMAnalogInputPort( + port=("con1", 1, 2), band=1, downconverter_frequency=5e9 + ) + + assert port.port == ("con1", 1, 2) + assert port.port_type == "analog_input" + assert port.band == 1 + assert port.downconverter_frequency == 5e9 + assert port.sampling_rate == 1e9 + assert port.shareable == False + + assert port.get_port_properties() == { + "band": 1, + "downconverter_frequency": 5e9, + "sampling_rate": 1e9, + "shareable": False, + } + + cfg = {"controllers": {}} + port.apply_to_config(cfg) + + assert cfg == { + "controllers": { + "con1": { + "fems": { + 1: { + "analog_inputs": { + 2: { + "band": 1, + "downconverter_frequency": 5e9, + "sampling_rate": 1e9, + "shareable": False, + } + } + } + } + } + } + } diff --git a/tests/components/ports/test_opx_ports.py b/tests/components/ports/test_opx_ports.py new file mode 100644 index 00000000..105e9ca3 --- /dev/null +++ b/tests/components/ports/test_opx_ports.py @@ -0,0 +1,85 @@ +import pytest +from quam.components.ports import ( + OPXPlusAnalogInputPort, + OPXPlusAnalogOutputPort, + OPXPlusDigitalOutputPort, +) + + +def test_opx_plus_analog_output_port(): + with pytest.raises(TypeError): + OPXPlusAnalogOutputPort() + + port = OPXPlusAnalogOutputPort(port=("con1", 2)) + assert port.port == ("con1", 2) + assert port.port_type == "analog_output" + assert port.offset == 0.0 + assert port.delay == 0 + assert port.crosstalk == {} + assert port.feedforward_filter == [] + assert port.feedback_filter == [] + assert port.shareable == False + + assert port.get_port_properties() == { + "offset": 0.0, + "delay": 0, + "crosstalk": {}, + "feedforward_filter": [], + "feedback_filter": [], + "shareable": False, + } + + cfg = {"controllers": {}} + port.apply_to_config(cfg) + + assert cfg == { + "controllers": { + "con1": { + "analog_outputs": { + 2: { + "offset": 0.0, + "delay": 0, + "crosstalk": {}, + "feedforward_filter": [], + "feedback_filter": [], + "shareable": False, + } + } + } + } + } + + +def test_opx_plus_analog_input_port(): + with pytest.raises(TypeError): + OPXPlusAnalogInputPort() + + port = OPXPlusAnalogInputPort(port=("con1", 2)) + assert port.port == ("con1", 2) + assert port.port_type == "analog_input" + assert port.offset == 0.0 + assert port.gain_db == 0 + assert port.shareable == False + + assert port.get_port_properties() == { + "offset": 0.0, + "gain_db": 0, + "shareable": False, + } + + cfg = {"controllers": {}} + port.apply_to_config(cfg) + + assert cfg == { + "controllers": { + "con1": { + "analog_inputs": { + 2: { + "offset": 0.0, + "gain_db": 0, + "shareable": False, + } + } + } + } + } From eb2f900757a95302358858ac2cf1e5ea0c09f800 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 28 Jun 2024 15:24:12 +0200 Subject: [PATCH 10/31] add documentation --- docs/components/channel-ports.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/components/channel-ports.md diff --git a/docs/components/channel-ports.md b/docs/components/channel-ports.md new file mode 100644 index 00000000..c4eab77a --- /dev/null +++ b/docs/components/channel-ports.md @@ -0,0 +1,25 @@ +# Channel Ports + +In the section [Channels](channels.md), we have seen how to create analog channels and attach digital outputs to them. +In these examples, the ports are defined by the OPX output tuple `(connector, port)`. +However, for more advanced use cases it is instead possible to define the ports using dedicated [Port][quam.components.ports.Port] QuAM components. +This is primarily useful in two situations: + +1. Multiple channels are connected to the same physical port, and the user wants to define the port and its properties only once. +2. The user wants to access port-specific properties that cannot directly be accessed through the [Channel][quam.components.channels.Channel]. Examples are the crosstalk, delay, and sampling rate of the port. + +## Example: Defining a Port +In this example, we want to use analog output ("con1", 3) of the OPX+ to create a single channel. +We define the port using the [OPXPlusAnalogOutputPort][quam.components.ports.OPXPlusAnalogOutputPort] component, which allows us to set the offset and delay of the port. + +```python +from quam.components.ports import OPXPlusAnalogOutputPort +from quam.components import SingleChannel + +port = OPXPlusAnalogOutputPort(port=("con1", 3), offset=0.2, delay=12) +channel = SingleChannel(opx_output=port) +``` + +Note that in this situation the port offset is defined by `channel.port.offset`, and is therefore part of the port. +The [SingleChannel][quam.components.channels.SingleChannel] component also has the attribute `SingleChannel.OPX_output_offset`, but its value is ignored in this case. +If we would have instead used `SingleChannel.opx_output = ("con1", 3)`, then the offset would have been defined by `channel.OPX_output_offset`. \ No newline at end of file From 3295e70a8c3f7dca8ed8909dba705ce3e4e9f9b5 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Fri, 28 Jun 2024 15:24:22 +0200 Subject: [PATCH 11/31] handling of offsets --- quam/components/ports.py | 6 ++++-- .../ports/test_lf_fem_analog_ports.py | 17 ++++++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/quam/components/ports.py b/quam/components/ports.py index 6d16de61..bc23611b 100644 --- a/quam/components/ports.py +++ b/quam/components/ports.py @@ -114,14 +114,16 @@ class LFAnalogOutputPort(QuamComponent, ABC): shareable: bool = False def get_port_properties(self): - return { - "offset": self.offset, + port_properties = { "delay": self.delay, "crosstalk": self.crosstalk, "feedforward_filter": self.feedforward_filter, "feedback_filter": self.feedback_filter, "shareable": self.shareable, } + if self.offset is not None: + port_properties["offset"] = self.offset + return port_properties @quam_dataclass diff --git a/tests/components/ports/test_lf_fem_analog_ports.py b/tests/components/ports/test_lf_fem_analog_ports.py index fb5ef491..eb7fcfd9 100644 --- a/tests/components/ports/test_lf_fem_analog_ports.py +++ b/tests/components/ports/test_lf_fem_analog_ports.py @@ -10,7 +10,7 @@ def test_lf_fem_analog_output_port(): port = LFFEMAnalogOutputPort(port=("con1", 1, 2)) assert port.port == ("con1", 1, 2) assert port.port_type == "analog_output" - assert port.offset == 0.0 + assert port.offset == None assert port.delay == 0 assert port.crosstalk == {} assert port.feedforward_filter == [] @@ -21,7 +21,6 @@ def test_lf_fem_analog_output_port(): assert port.upsampling_mode == "mw" assert port.get_port_properties() == { - "offset": 0.0, "delay": 0, "crosstalk": {}, "feedforward_filter": [], @@ -42,7 +41,6 @@ def test_lf_fem_analog_output_port(): 1: { "analog_outputs": { 2: { - "offset": 0.0, "delay": 0, "crosstalk": {}, "feedforward_filter": [], @@ -59,6 +57,19 @@ def test_lf_fem_analog_output_port(): } } + port.offset = 0.1 + assert port.get_port_properties() == { + "delay": 0, + "crosstalk": {}, + "feedforward_filter": [], + "feedback_filter": [], + "shareable": False, + "output_mode": "direct", + "sampling_rate": 1e9, + "upsampling_mode": "mw", + "offset": 0.1, + } + def test_lf_fem_analog_input_port(): with pytest.raises(TypeError): From e70a49933564f7975d6e287cb666b99a798d5b6e Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Sun, 30 Jun 2024 13:11:35 +0200 Subject: [PATCH 12/31] MWChannel inherits from Channel --- quam/components/channels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quam/components/channels.py b/quam/components/channels.py index 31086ee5..fbdd5288 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -1482,7 +1482,7 @@ class InIQOutSingleChannel(SingleChannel, InIQChannel): @quam_dataclass -class MWChannel(QuamComponent): +class MWChannel(Channel): opx_output: MWFEMAnalogOutputPort upconverter: int = 1 @@ -1495,7 +1495,7 @@ def apply_to_config(self, config: Dict) -> None: @quam_dataclass -class InMWChannel(QuamComponent): +class InMWChannel(Channel): opx_input: MWFEMAnalogInputPort def apply_to_config(self, config: Dict) -> None: From 849482e4b34ea140b1fae66529a9335fe61c8c4f Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Sun, 30 Jun 2024 20:54:30 +0200 Subject: [PATCH 13/31] refactoring of ports --- quam/components/channels.py | 20 +- quam/components/ports.py | 266 ------------------ quam/components/ports/__init__.py | 0 quam/components/ports/analog_inputs.py | 62 ++++ quam/components/ports/analog_outputs.py | 84 ++++++ quam/components/ports/base_ports.py | 110 ++++++++ quam/components/ports/digital_inputs.py | 25 ++ quam/components/ports/digital_outputs.py | 34 +++ tests/components/ports/test_digital_ports.py | 6 +- .../ports/test_lf_fem_analog_ports.py | 3 +- .../ports/test_mw_fem_analog_ports.py | 3 +- tests/components/ports/test_opx_ports.py | 6 +- 12 files changed, 339 insertions(+), 280 deletions(-) delete mode 100644 quam/components/ports.py create mode 100644 quam/components/ports/__init__.py create mode 100644 quam/components/ports/analog_inputs.py create mode 100644 quam/components/ports/analog_outputs.py create mode 100644 quam/components/ports/base_ports.py create mode 100644 quam/components/ports/digital_inputs.py create mode 100644 quam/components/ports/digital_outputs.py diff --git a/quam/components/channels.py b/quam/components/channels.py index fbdd5288..452968de 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -3,19 +3,25 @@ import warnings from quam.components.hardware import BaseFrequencyConverter, Mixer, LocalOscillator -from quam.components.pulses import Pulse, BaseReadoutPulse -from quam.components.ports import ( +from quam.components.ports.digital_outputs import ( DigitalOutputPort, - FEMDigitalOutputPort, - LFAnalogOutputPort, + OPXPlusDigitalOutputPort, +) +from quam.components.ports.analog_inputs import ( LFAnalogInputPort, LFFEMAnalogInputPort, - LFFEMAnalogOutputPort, MWFEMAnalogInputPort, - MWFEMAnalogOutputPort, OPXPlusAnalogInputPort, +) +from quam.components.ports.analog_outputs import ( + LFAnalogOutputPort, + LFFEMAnalogOutputPort, + MWFEMAnalogOutputPort, OPXPlusAnalogOutputPort, - OPXPlusDigitalOutputPort, +) +from quam.components.pulses import Pulse, BaseReadoutPulse +from quam.components.ports.digital_outputs import ( + FEMDigitalOutputPort, ) from quam.core import QuamComponent, quam_dataclass from quam.core.quam_classes import QuamDict diff --git a/quam/components/ports.py b/quam/components/ports.py deleted file mode 100644 index bc23611b..00000000 --- a/quam/components/ports.py +++ /dev/null @@ -1,266 +0,0 @@ -from abc import ABC, abstractmethod -from dataclasses import field -from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple, Union -import warnings -from quam.core import QuamComponent, quam_dataclass - - -# ---- General ports ---- # -@quam_dataclass -class Port(QuamComponent, ABC): - port_type: ClassVar[str] - port: Union[Tuple[str, int], Tuple[str, int, int]] - - @abstractmethod - def get_port_config( - self, config: Dict[str, Any], create: bool = True - ) -> Dict[str, Any]: - pass - - @abstractmethod - def get_port_properties(self) -> Dict[str, Any]: - pass - - @staticmethod - def _update_port_config(port_config, port_properties): - for key, value in port_properties.items(): - try: - if key in port_config and value != port_config[key]: - warnings.warn( - f"Error generating QUA config: Controller {self.port_type} " - f"port {self.port} already has entry for {key}. This likely " - f"means that the port is being configured multiple times. " - f"Overwriting {port_config['key']} → {value}." - ) - except Exception: - pass - port_config[key] = value - - def apply_to_config(self, config: Dict) -> None: - super().apply_to_config(config) - - port_cfg = self.get_port_config(config) - port_properties = self.get_port_properties() - self._update_port_config(port_cfg, port_properties) - - -@quam_dataclass -class OPXPlusPort(Port, ABC): - port: Tuple[str, int] - - def get_port_config( - self, config: Dict[str, Any], create: bool = True - ) -> Dict[str, Any]: - controller_name, port = self.port - - if not create: - try: - return config["controllers"][controller_name][f"{self.port_type}"][port] - except KeyError: - raise KeyError( - f"Error generating config: controller {controller_name} does not " - f"have entry {self.port_type}s for port {self.port}" - ) - - controller_cfg = config["controllers"].setdefault(controller_name, {}) - ports_cfg = controller_cfg.setdefault(f"{self.port_type}s", {}) - port_cfg = ports_cfg.setdefault(port, {}) - return port_cfg - - -@quam_dataclass -class FEMPort(Port, ABC): - port: Tuple[str, int, int] - - def get_port_config( - self, config: Dict[str, Any], create: bool = True - ) -> Dict[str, Any]: - controller_name, fem, port = self.port - - if not create: - try: - fem_cfg = config["controllers"][controller_name]["fems"][fem] - except KeyError: - raise KeyError( - f"Error generating config: controller {controller_name} does not " - f"have entry for FEM {fem} for port {self.port}" - ) - try: - return fem_cfg[f"{self.port_type}s"][port] - except KeyError: - raise KeyError( - f"Error generating config: controller {controller_name} does not " - f"have entry {self.port_type}s for port {self.port}" - ) - - controller_cfg = config["controllers"].setdefault(controller_name, {}) - fems_cfg = controller_cfg.setdefault("fems", {}) - fem_cfg = fems_cfg.setdefault(fem, {}) - ports_cfg = fem_cfg.setdefault(f"{self.port_type}s", {}) - port_cfg = ports_cfg.setdefault(port, {}) - return port_cfg - - -# --- Analog ports --- # -@quam_dataclass -class LFAnalogOutputPort(QuamComponent, ABC): - port_type: ClassVar[str] = "analog_output" - - offset: float = 0.0 - delay: int = 0 - crosstalk: Dict[int, float] = field(default_factory=dict) - feedforward_filter: List[float] = field(default_factory=list) - feedback_filter: List[float] = field(default_factory=list) - shareable: bool = False - - def get_port_properties(self): - port_properties = { - "delay": self.delay, - "crosstalk": self.crosstalk, - "feedforward_filter": self.feedforward_filter, - "feedback_filter": self.feedback_filter, - "shareable": self.shareable, - } - if self.offset is not None: - port_properties["offset"] = self.offset - return port_properties - - -@quam_dataclass -class LFAnalogInputPort(QuamComponent, ABC): - port_type: ClassVar[str] = "analog_input" - - offset: float = 0.0 - gain_db: int = 0 - shareable: bool = False - - def get_port_properties(self): - return { - "offset": self.offset, - "gain_db": self.gain_db, - "shareable": self.shareable, - } - - -@quam_dataclass -class OPXPlusAnalogOutputPort(LFAnalogOutputPort, OPXPlusPort): - pass - - -@quam_dataclass -class OPXPlusAnalogInputPort(LFAnalogInputPort, OPXPlusPort): - pass - - -@quam_dataclass -class LFFEMAnalogOutputPort(LFAnalogOutputPort, FEMPort): - sampling_rate: float = 1e9 # Either 1e9 or 2e9 - upsampling_mode: Literal["mw", "pulse"] = "mw" - output_mode: Literal["direct", "amplified"] = "direct" - - def get_port_properties(self) -> Dict[str, Any]: - port_properties = super().get_port_properties() - port_properties["sampling_rate"] = self.sampling_rate - port_properties["upsampling_mode"] = self.upsampling_mode - port_properties["output_mode"] = self.output_mode - return port_properties - - -@quam_dataclass -class LFFEMAnalogInputPort(LFAnalogInputPort, FEMPort): - sampling_rate: float = 1e9 # Either 1e9 or 2e9 - - def get_port_properties(self) -> Dict[str, Any]: - port_properties = super().get_port_properties() - port_properties["sampling_rate"] = self.sampling_rate - return port_properties - - -@quam_dataclass -class MWFEMAnalogOutputPort(FEMPort): - port_type: ClassVar[str] = "analog_output" - - band: int - upconverter_frequency: Optional[float] = None - upconverters: Optional[Dict[int, float]] = None - delay: int = 0 - shareable: bool = False - sampling_rate: float = 1e9 # Either 1e9 or 2e9 - full_scale_power_dbm: int = -11 - - def get_port_properties(self) -> Dict[str, Any]: - port_cfg = { - "band": self.band, - "delay": self.delay, - "shareable": self.shareable, - "sampling_rate": self.sampling_rate, - "full_scale_power_dbm": self.full_scale_power_dbm, - } - if self.upconverter_frequency is not None: - port_cfg["upconverter_frequency"] = self.upconverter_frequency - if self.upconverters is not None: - port_cfg["upconverters"] = self.upconverters - return port_cfg - - -@quam_dataclass -class MWFEMAnalogInputPort(FEMPort): - port_type: ClassVar[str] = "analog_input" - - band: int - downconverter_frequency: float - sampling_rate: float = 1e9 # Either 1e9 or 2e9 - shareable: bool = False - - def get_port_properties(self) -> Dict[str, Any]: - return { - "band": self.band, - "downconverter_frequency": self.downconverter_frequency, - "sampling_rate": self.sampling_rate, - "shareable": self.shareable, - } - - -# --- Digital ports --- # -@quam_dataclass -class DigitalOutputPort(QuamComponent, ABC): - port_type: ClassVar[str] = "digital_output" - - inverted: bool = False - shareable: bool = False - - def get_port_properties(self) -> Dict[str, Any]: - return {"inverted": self.inverted, "shareable": self.shareable} - - -@quam_dataclass -class OPXPlusDigitalOutputPort(DigitalOutputPort, OPXPlusPort): - pass - - -@quam_dataclass -class OPXPlusDigitalInputPort(OPXPlusPort): - port_type: ClassVar[str] = "digital_input" - - deadtime: int = 4 - polarity: Literal["rising", "falling"] = "rising" - threshold: float = 2.0 - shareable: bool = False - - def get_port_properties(self) -> Dict[str, Any]: - return { - "deadtime": self.deadtime, - "polarity": self.polarity, - "threshold": self.threshold, - "shareable": self.shareable, - } - - -@quam_dataclass -class FEMDigitalOutputPort(DigitalOutputPort, FEMPort): - level: Literal["TTL", "LVTTL"] = "LVTTL" - - def get_port_properties(self) -> Dict[str, Any]: - port_properties = super().get_port_properties() - port_properties["level"] = self.level - return port_properties diff --git a/quam/components/ports/__init__.py b/quam/components/ports/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/quam/components/ports/analog_inputs.py b/quam/components/ports/analog_inputs.py new file mode 100644 index 00000000..249722b7 --- /dev/null +++ b/quam/components/ports/analog_inputs.py @@ -0,0 +1,62 @@ +from abc import ABC +from typing import Any, ClassVar, Dict + +from quam.components.ports.base_ports import BasePort, FEMPort, OPXPlusPort +from quam.core import quam_dataclass + + +__all__ = [ + "LFAnalogInputPort", + "OPXPlusAnalogInputPort", + "LFFEMAnalogInputPort", + "MWFEMAnalogInputPort", +] + + +@quam_dataclass +class LFAnalogInputPort(BasePort, ABC): + port_type: ClassVar[str] = "analog_input" + + offset: float = 0.0 + gain_db: int = 0 + shareable: bool = False + + def get_port_properties(self): + return { + "offset": self.offset, + "gain_db": self.gain_db, + "shareable": self.shareable, + } + + +@quam_dataclass +class OPXPlusAnalogInputPort(LFAnalogInputPort, OPXPlusPort): + pass + + +@quam_dataclass +class LFFEMAnalogInputPort(LFAnalogInputPort, FEMPort): + sampling_rate: float = 1e9 # Either 1e9 or 2e9 + + def get_port_properties(self) -> Dict[str, Any]: + port_properties = super().get_port_properties() + port_properties["sampling_rate"] = self.sampling_rate + return port_properties + + +@quam_dataclass +class MWFEMAnalogInputPort(FEMPort): + port_type: ClassVar[str] = "analog_input" + + band: int + downconverter_frequency: float + sampling_rate: float = 1e9 # Either 1e9 or 2e9 + shareable: bool = False + + def get_port_properties(self) -> Dict[str, Any]: + return { + "band": self.band, + "downconverter_frequency": self.downconverter_frequency, + "sampling_rate": self.sampling_rate, + "shareable": self.shareable, + } diff --git a/quam/components/ports/analog_outputs.py b/quam/components/ports/analog_outputs.py new file mode 100644 index 00000000..17f60593 --- /dev/null +++ b/quam/components/ports/analog_outputs.py @@ -0,0 +1,84 @@ +from abc import ABC +from dataclasses import field +from typing import Any, ClassVar, Dict, List, Literal, Optional + +from quam.components.ports.base_ports import BasePort, FEMPort, OPXPlusPort +from quam.core import quam_dataclass + + +__all__ = [ + "LFAnalogOutputPort", + "OPXPlusAnalogOutputPort", + "LFFEMAnalogOutputPort", + "MWFEMAnalogOutputPort", +] + + +@quam_dataclass +class LFAnalogOutputPort(BasePort, ABC): + port_type: ClassVar[str] = "analog_output" + + offset: float = 0.0 + delay: int = 0 + crosstalk: Dict[int, float] = field(default_factory=dict) + feedforward_filter: List[float] = field(default_factory=list) + feedback_filter: List[float] = field(default_factory=list) + shareable: bool = False + + def get_port_properties(self): + port_properties = { + "delay": self.delay, + "crosstalk": self.crosstalk, + "feedforward_filter": self.feedforward_filter, + "feedback_filter": self.feedback_filter, + "shareable": self.shareable, + } + if self.offset is not None: + port_properties["offset"] = self.offset + return port_properties + + +@quam_dataclass +class OPXPlusAnalogOutputPort(LFAnalogOutputPort, OPXPlusPort): + pass + + +@quam_dataclass +class LFFEMAnalogOutputPort(LFAnalogOutputPort, FEMPort): + sampling_rate: float = 1e9 # Either 1e9 or 2e9 + upsampling_mode: Literal["mw", "pulse"] = "mw" + output_mode: Literal["direct", "amplified"] = "direct" + + def get_port_properties(self) -> Dict[str, Any]: + port_properties = super().get_port_properties() + port_properties["sampling_rate"] = self.sampling_rate + port_properties["upsampling_mode"] = self.upsampling_mode + port_properties["output_mode"] = self.output_mode + return port_properties + + +@quam_dataclass +class MWFEMAnalogOutputPort(FEMPort): + port_type: ClassVar[str] = "analog_output" + + band: int + upconverter_frequency: Optional[float] = None + upconverters: Optional[Dict[int, float]] = None + delay: int = 0 + shareable: bool = False + sampling_rate: float = 1e9 # Either 1e9 or 2e9 + full_scale_power_dbm: int = -11 + + def get_port_properties(self) -> Dict[str, Any]: + port_cfg = { + "band": self.band, + "delay": self.delay, + "shareable": self.shareable, + "sampling_rate": self.sampling_rate, + "full_scale_power_dbm": self.full_scale_power_dbm, + } + if self.upconverter_frequency is not None: + port_cfg["upconverter_frequency"] = self.upconverter_frequency + if self.upconverters is not None: + port_cfg["upconverters"] = self.upconverters + return port_cfg diff --git a/quam/components/ports/base_ports.py b/quam/components/ports/base_ports.py new file mode 100644 index 00000000..5f0a8574 --- /dev/null +++ b/quam/components/ports/base_ports.py @@ -0,0 +1,110 @@ +import warnings +from abc import ABC, abstractmethod +from typing import Any, ClassVar, Dict, Tuple, Union + +from quam.core import QuamComponent, quam_dataclass + + +__all__ = ["BasePort", "OPXPlusPort", "FEMPort"] + + +@quam_dataclass +class BasePort(QuamComponent, ABC): + port_type: ClassVar[str] + controller_name: str + port_number: int + + @abstractmethod + def get_port_config( + self, config: Dict[str, Any], create: bool = True + ) -> Dict[str, Any]: + pass + + @abstractmethod + def get_port_properties(self) -> Dict[str, Any]: + pass + + @property + @abstractmethod + def port_id(self) -> Union[Tuple[str, int], Tuple[str, int, int]]: + pass + + def _update_port_config(self, port_config, port_properties): + for key, value in port_properties.items(): + try: + if key in port_config and value != port_config[key]: + warnings.warn( + f"Error generating QUA config: Controller {self.port_type} " + f"port {self.port_id} already has entry for {key}. This likely " + f"means that the port is being configured multiple times. " + f"Overwriting {port_config['key']} → {value}." + ) + except Exception: + pass + port_config[key] = value + + def apply_to_config(self, config: Dict) -> None: + super().apply_to_config(config) + + port_cfg = self.get_port_config(config) + port_properties = self.get_port_properties() + self._update_port_config(port_cfg, port_properties) + + +@quam_dataclass +class OPXPlusPort(BasePort, ABC): + + def get_port_config( + self, config: Dict[str, Any], create: bool = True + ) -> Dict[str, Any]: + + if not create: + try: + controller_cfg = config["controllers"][self.controller_name] + return controller_cfg[f"{self.port_type}"][self.port_number] + except KeyError: + raise KeyError( + f"Error generating config: controller {self.controller_name} does " + f"not have entry {self.port_type}s for port {self.port_id}" + ) + + controller_cfg = config["controllers"].setdefault(self.controller_name, {}) + ports_cfg = controller_cfg.setdefault(f"{self.port_type}s", {}) + port_cfg = ports_cfg.setdefault(self.port_number, {}) + return port_cfg + + +@quam_dataclass +class FEMPort(BasePort, ABC): + controller_name: str + fem_number: int + port_number: int + + def get_port_config( + self, config: Dict[str, Any], create: bool = True + ) -> Dict[str, Any]: + + if not create: + try: + controller_cfg = config["controllers"][self.controller_name] + fem_cfg = controller_cfg["fems"][self.fem_number] + except KeyError: + raise KeyError( + f"Error generating config: controller {self.controller_name} does " + f"not have entry for FEM {self.fem_number} for " + f"port {self.port_number}" + ) + try: + return fem_cfg[f"{self.port_type}s"][self.port_number] + except KeyError: + raise KeyError( + f"Error generating config: controller {self.controller_name} does " + f"not have entry {self.port_type}s for port {self.port_id}" + ) + + controller_cfg = config["controllers"].setdefault(self.controller_name, {}) + fems_cfg = controller_cfg.setdefault("fems", {}) + fem_cfg = fems_cfg.setdefault(self.fem_number, {}) + ports_cfg = fem_cfg.setdefault(f"{self.port_type}s", {}) + port_cfg = ports_cfg.setdefault(self.port_number, {}) + return port_cfg diff --git a/quam/components/ports/digital_inputs.py b/quam/components/ports/digital_inputs.py new file mode 100644 index 00000000..fc681676 --- /dev/null +++ b/quam/components/ports/digital_inputs.py @@ -0,0 +1,25 @@ +from typing import Any, ClassVar, Dict, Literal + +from quam.components.ports.base_ports import OPXPlusPort +from quam.core import quam_dataclass + + +__all__ = ["OPXPlusDigitalInputPort"] + + +@quam_dataclass +class OPXPlusDigitalInputPort(OPXPlusPort): + port_type: ClassVar[str] = "digital_input" + + deadtime: int = 4 + polarity: Literal["rising", "falling"] = "rising" + threshold: float = 2.0 + shareable: bool = False + + def get_port_properties(self) -> Dict[str, Any]: + return { + "deadtime": self.deadtime, + "polarity": self.polarity, + "threshold": self.threshold, + "shareable": self.shareable, + } diff --git a/quam/components/ports/digital_outputs.py b/quam/components/ports/digital_outputs.py new file mode 100644 index 00000000..81f62f74 --- /dev/null +++ b/quam/components/ports/digital_outputs.py @@ -0,0 +1,34 @@ +from abc import ABC +from typing import Any, ClassVar, Dict, Literal + +from quam.components.ports.base_ports import BasePort, FEMPort, OPXPlusPort +from quam.core import quam_dataclass + + +__all__ = ["DigitalOutputPort", "OPXPlusDigitalOutputPort", "FEMDigitalOutputPort"] + + +@quam_dataclass +class DigitalOutputPort(BasePort, ABC): + port_type: ClassVar[str] = "digital_output" + + inverted: bool = False + shareable: bool = False + + def get_port_properties(self) -> Dict[str, Any]: + return {"inverted": self.inverted, "shareable": self.shareable} + + +@quam_dataclass +class OPXPlusDigitalOutputPort(DigitalOutputPort, OPXPlusPort): + pass + + +@quam_dataclass +class FEMDigitalOutputPort(DigitalOutputPort, FEMPort): + level: Literal["TTL", "LVTTL"] = "LVTTL" + + def get_port_properties(self) -> Dict[str, Any]: + port_properties = super().get_port_properties() + port_properties["level"] = self.level + return port_properties diff --git a/tests/components/ports/test_digital_ports.py b/tests/components/ports/test_digital_ports.py index fb739da7..1e0a7956 100644 --- a/tests/components/ports/test_digital_ports.py +++ b/tests/components/ports/test_digital_ports.py @@ -1,10 +1,12 @@ import pytest -from quam.components.ports import ( +from quam.components.ports.digital_outputs import ( FEMDigitalOutputPort, - OPXPlusDigitalInputPort, OPXPlusDigitalOutputPort, ) +from quam.components.ports.digital_inputs import ( + OPXPlusDigitalInputPort, +) def test_opx_plus_digital_output_port(): diff --git a/tests/components/ports/test_lf_fem_analog_ports.py b/tests/components/ports/test_lf_fem_analog_ports.py index eb7fcfd9..089db71e 100644 --- a/tests/components/ports/test_lf_fem_analog_ports.py +++ b/tests/components/ports/test_lf_fem_analog_ports.py @@ -1,6 +1,7 @@ import pytest -from quam.components.ports import LFFEMAnalogInputPort, LFFEMAnalogOutputPort +from quam.components.ports.analog_outputs import LFFEMAnalogOutputPort +from quam.components.ports.analog_inputs import LFFEMAnalogInputPort def test_lf_fem_analog_output_port(): diff --git a/tests/components/ports/test_mw_fem_analog_ports.py b/tests/components/ports/test_mw_fem_analog_ports.py index 43382a9a..3b031aee 100644 --- a/tests/components/ports/test_mw_fem_analog_ports.py +++ b/tests/components/ports/test_mw_fem_analog_ports.py @@ -1,6 +1,7 @@ import pytest -from quam.components.ports import MWFEMAnalogInputPort, MWFEMAnalogOutputPort +from quam.components.ports.analog_outputs import MWFEMAnalogOutputPort +from quam.components.ports.analog_inputs import MWFEMAnalogInputPort def test_mw_fem_analog_output_port(): diff --git a/tests/components/ports/test_opx_ports.py b/tests/components/ports/test_opx_ports.py index 105e9ca3..bc6a170f 100644 --- a/tests/components/ports/test_opx_ports.py +++ b/tests/components/ports/test_opx_ports.py @@ -1,7 +1,7 @@ import pytest -from quam.components.ports import ( - OPXPlusAnalogInputPort, - OPXPlusAnalogOutputPort, +from quam.components.ports.analog_inputs import OPXPlusAnalogInputPort +from quam.components.ports.analog_outputs import OPXPlusAnalogOutputPort +from quam.components.ports.digital_outputs import ( OPXPlusDigitalOutputPort, ) From a5bb6cbfab894f921858c3d2248a31bba33de23e Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Sun, 30 Jun 2024 20:56:22 +0200 Subject: [PATCH 14/31] renaming --- quam/components/ports/base_ports.py | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/quam/components/ports/base_ports.py b/quam/components/ports/base_ports.py index 5f0a8574..7fc08aa5 100644 --- a/quam/components/ports/base_ports.py +++ b/quam/components/ports/base_ports.py @@ -12,7 +12,7 @@ class BasePort(QuamComponent, ABC): port_type: ClassVar[str] controller_name: str - port_number: int + port_id: int @abstractmethod def get_port_config( @@ -26,7 +26,7 @@ def get_port_properties(self) -> Dict[str, Any]: @property @abstractmethod - def port_id(self) -> Union[Tuple[str, int], Tuple[str, int, int]]: + def port_tuple(self) -> Union[Tuple[str, int], Tuple[str, int, int]]: pass def _update_port_config(self, port_config, port_properties): @@ -35,7 +35,7 @@ def _update_port_config(self, port_config, port_properties): if key in port_config and value != port_config[key]: warnings.warn( f"Error generating QUA config: Controller {self.port_type} " - f"port {self.port_id} already has entry for {key}. This likely " + f"port {self.port_tuple} already has entry for {key}. This likely " f"means that the port is being configured multiple times. " f"Overwriting {port_config['key']} → {value}." ) @@ -61,24 +61,24 @@ def get_port_config( if not create: try: controller_cfg = config["controllers"][self.controller_name] - return controller_cfg[f"{self.port_type}"][self.port_number] + return controller_cfg[f"{self.port_type}"][self.port_id] except KeyError: raise KeyError( f"Error generating config: controller {self.controller_name} does " - f"not have entry {self.port_type}s for port {self.port_id}" + f"not have entry {self.port_type}s for port {self.port_tuple}" ) controller_cfg = config["controllers"].setdefault(self.controller_name, {}) ports_cfg = controller_cfg.setdefault(f"{self.port_type}s", {}) - port_cfg = ports_cfg.setdefault(self.port_number, {}) + port_cfg = ports_cfg.setdefault(self.port_id, {}) return port_cfg @quam_dataclass class FEMPort(BasePort, ABC): controller_name: str - fem_number: int - port_number: int + fem_id: int + port_id: int def get_port_config( self, config: Dict[str, Any], create: bool = True @@ -87,24 +87,24 @@ def get_port_config( if not create: try: controller_cfg = config["controllers"][self.controller_name] - fem_cfg = controller_cfg["fems"][self.fem_number] + fem_cfg = controller_cfg["fems"][self.fem_id] except KeyError: raise KeyError( f"Error generating config: controller {self.controller_name} does " - f"not have entry for FEM {self.fem_number} for " - f"port {self.port_number}" + f"not have entry for FEM {self.fem_id} for " + f"port {self.port_id}" ) try: - return fem_cfg[f"{self.port_type}s"][self.port_number] + return fem_cfg[f"{self.port_type}s"][self.port_id] except KeyError: raise KeyError( f"Error generating config: controller {self.controller_name} does " - f"not have entry {self.port_type}s for port {self.port_id}" + f"not have entry {self.port_type}s for port {self.port_tuple}" ) controller_cfg = config["controllers"].setdefault(self.controller_name, {}) fems_cfg = controller_cfg.setdefault("fems", {}) - fem_cfg = fems_cfg.setdefault(self.fem_number, {}) + fem_cfg = fems_cfg.setdefault(self.fem_id, {}) ports_cfg = fem_cfg.setdefault(f"{self.port_type}s", {}) - port_cfg = ports_cfg.setdefault(self.port_number, {}) + port_cfg = ports_cfg.setdefault(self.port_id, {}) return port_cfg From 63b1152b4a0bb1b861a31d77eb17d2f2058338a7 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Sun, 30 Jun 2024 21:02:28 +0200 Subject: [PATCH 15/31] move paraemters between classes --- quam/components/ports/base_ports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quam/components/ports/base_ports.py b/quam/components/ports/base_ports.py index 7fc08aa5..8c3d8f2c 100644 --- a/quam/components/ports/base_ports.py +++ b/quam/components/ports/base_ports.py @@ -11,8 +11,6 @@ @quam_dataclass class BasePort(QuamComponent, ABC): port_type: ClassVar[str] - controller_name: str - port_id: int @abstractmethod def get_port_config( @@ -53,6 +51,8 @@ def apply_to_config(self, config: Dict) -> None: @quam_dataclass class OPXPlusPort(BasePort, ABC): + controller_name: str + port_id: int def get_port_config( self, config: Dict[str, Any], create: bool = True From 2edcb0ecb9bed32e29026155cac1bd131f047472 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Sun, 30 Jun 2024 21:03:49 +0200 Subject: [PATCH 16/31] rename controller_name -> controller_id --- quam/components/ports/base_ports.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/quam/components/ports/base_ports.py b/quam/components/ports/base_ports.py index 8c3d8f2c..201e0585 100644 --- a/quam/components/ports/base_ports.py +++ b/quam/components/ports/base_ports.py @@ -51,7 +51,7 @@ def apply_to_config(self, config: Dict) -> None: @quam_dataclass class OPXPlusPort(BasePort, ABC): - controller_name: str + controller_id: Union[str, int] port_id: int def get_port_config( @@ -60,15 +60,15 @@ def get_port_config( if not create: try: - controller_cfg = config["controllers"][self.controller_name] + controller_cfg = config["controllers"][self.controller_id] return controller_cfg[f"{self.port_type}"][self.port_id] except KeyError: raise KeyError( - f"Error generating config: controller {self.controller_name} does " + f"Error generating config: controller {self.controller_id} does " f"not have entry {self.port_type}s for port {self.port_tuple}" ) - controller_cfg = config["controllers"].setdefault(self.controller_name, {}) + controller_cfg = config["controllers"].setdefault(self.controller_id, {}) ports_cfg = controller_cfg.setdefault(f"{self.port_type}s", {}) port_cfg = ports_cfg.setdefault(self.port_id, {}) return port_cfg @@ -76,7 +76,7 @@ def get_port_config( @quam_dataclass class FEMPort(BasePort, ABC): - controller_name: str + controller_id: Union[str, int] fem_id: int port_id: int @@ -86,11 +86,11 @@ def get_port_config( if not create: try: - controller_cfg = config["controllers"][self.controller_name] + controller_cfg = config["controllers"][self.controller_id] fem_cfg = controller_cfg["fems"][self.fem_id] except KeyError: raise KeyError( - f"Error generating config: controller {self.controller_name} does " + f"Error generating config: controller {self.controller_id} does " f"not have entry for FEM {self.fem_id} for " f"port {self.port_id}" ) @@ -98,11 +98,11 @@ def get_port_config( return fem_cfg[f"{self.port_type}s"][self.port_id] except KeyError: raise KeyError( - f"Error generating config: controller {self.controller_name} does " + f"Error generating config: controller {self.controller_id} does " f"not have entry {self.port_type}s for port {self.port_tuple}" ) - controller_cfg = config["controllers"].setdefault(self.controller_name, {}) + controller_cfg = config["controllers"].setdefault(self.controller_id, {}) fems_cfg = controller_cfg.setdefault("fems", {}) fem_cfg = fems_cfg.setdefault(self.fem_id, {}) ports_cfg = fem_cfg.setdefault(f"{self.port_type}s", {}) From 8afdd0ce0d49d925b55240f8d66764c397d8da88 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Mon, 1 Jul 2024 09:04:07 +0200 Subject: [PATCH 17/31] change crosstalk, filters default values --- quam/components/ports/analog_outputs.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/quam/components/ports/analog_outputs.py b/quam/components/ports/analog_outputs.py index 17f60593..c8e28395 100644 --- a/quam/components/ports/analog_outputs.py +++ b/quam/components/ports/analog_outputs.py @@ -20,19 +20,22 @@ class LFAnalogOutputPort(BasePort, ABC): offset: float = 0.0 delay: int = 0 - crosstalk: Dict[int, float] = field(default_factory=dict) - feedforward_filter: List[float] = field(default_factory=list) - feedback_filter: List[float] = field(default_factory=list) + crosstalk: Optional[Dict[int, float]] = None + feedforward_filter: Optional[List[float]] = None + feedback_filter: Optional[List[float]] = None shareable: bool = False def get_port_properties(self): port_properties = { "delay": self.delay, - "crosstalk": self.crosstalk, - "feedforward_filter": self.feedforward_filter, - "feedback_filter": self.feedback_filter, "shareable": self.shareable, } + if self.crosstalk is not None: + port_properties["crosstalk"] = self.crosstalk + if self.feedforward_filter is not None: + port_properties["feedforward_filter"] = self.feedforward_filter + if self.feedback_filter is not None: + port_properties["feedback_filter"] = self.feedback_filter if self.offset is not None: port_properties["offset"] = self.offset return port_properties @@ -69,6 +72,14 @@ class MWFEMAnalogOutputPort(FEMPort): sampling_rate: float = 1e9 # Either 1e9 or 2e9 full_scale_power_dbm: int = -11 + def __post_init__(self) -> None: + super().__post_init__() + if self.upconverter_frequency is None and self.upconverters is None: + raise ValueError( + "MWAnalogOutputPort: Either upconverter_frequency or upconverters must " + "be provided" + ) + def get_port_properties(self) -> Dict[str, Any]: port_cfg = { "band": self.band, From 0484896ab199e607b2d3896e5e0822322a53b80d Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Mon, 1 Jul 2024 20:59:47 +0200 Subject: [PATCH 18/31] add port tuple, made offset optional --- quam/components/ports/analog_inputs.py | 10 ++-- quam/components/ports/analog_outputs.py | 3 +- quam/components/ports/base_ports.py | 8 ++++ tests/components/ports/test_digital_ports.py | 39 ++++++++++++---- .../ports/test_lf_fem_analog_ports.py | 46 +++++++++++-------- .../ports/test_mw_fem_analog_ports.py | 46 ++++++++++++------- tests/components/ports/test_opx_ports.py | 45 +++++++++--------- 7 files changed, 126 insertions(+), 71 deletions(-) diff --git a/quam/components/ports/analog_inputs.py b/quam/components/ports/analog_inputs.py index 249722b7..b7d1675a 100644 --- a/quam/components/ports/analog_inputs.py +++ b/quam/components/ports/analog_inputs.py @@ -1,5 +1,5 @@ from abc import ABC -from typing import Any, ClassVar, Dict +from typing import Any, ClassVar, Dict, Optional from quam.components.ports.base_ports import BasePort, FEMPort, OPXPlusPort from quam.core import quam_dataclass @@ -17,16 +17,18 @@ class LFAnalogInputPort(BasePort, ABC): port_type: ClassVar[str] = "analog_input" - offset: float = 0.0 + offset: Optional[float] = None gain_db: int = 0 shareable: bool = False def get_port_properties(self): - return { - "offset": self.offset, + port_cfg = { "gain_db": self.gain_db, "shareable": self.shareable, } + if self.offset is not None: + port_cfg["offset"] = self.offset + return port_cfg @quam_dataclass diff --git a/quam/components/ports/analog_outputs.py b/quam/components/ports/analog_outputs.py index c8e28395..1e4a9c3b 100644 --- a/quam/components/ports/analog_outputs.py +++ b/quam/components/ports/analog_outputs.py @@ -1,5 +1,4 @@ from abc import ABC -from dataclasses import field from typing import Any, ClassVar, Dict, List, Literal, Optional from quam.components.ports.base_ports import BasePort, FEMPort, OPXPlusPort @@ -18,7 +17,7 @@ class LFAnalogOutputPort(BasePort, ABC): port_type: ClassVar[str] = "analog_output" - offset: float = 0.0 + offset: Optional[float] = None delay: int = 0 crosstalk: Optional[Dict[int, float]] = None feedforward_filter: Optional[List[float]] = None diff --git a/quam/components/ports/base_ports.py b/quam/components/ports/base_ports.py index 201e0585..cef03c05 100644 --- a/quam/components/ports/base_ports.py +++ b/quam/components/ports/base_ports.py @@ -54,6 +54,10 @@ class OPXPlusPort(BasePort, ABC): controller_id: Union[str, int] port_id: int + @property + def port_tuple(self) -> Tuple[Union[str, int], int]: + return self.controller_id, self.port_id + def get_port_config( self, config: Dict[str, Any], create: bool = True ) -> Dict[str, Any]: @@ -80,6 +84,10 @@ class FEMPort(BasePort, ABC): fem_id: int port_id: int + @property + def port_tuple(self) -> Tuple[Union[str, int], int, int]: + return self.controller_id, self.fem_id, self.port_id + def get_port_config( self, config: Dict[str, Any], create: bool = True ) -> Dict[str, Any]: diff --git a/tests/components/ports/test_digital_ports.py b/tests/components/ports/test_digital_ports.py index 1e0a7956..a14d8db9 100644 --- a/tests/components/ports/test_digital_ports.py +++ b/tests/components/ports/test_digital_ports.py @@ -13,11 +13,18 @@ def test_opx_plus_digital_output_port(): with pytest.raises(TypeError): OPXPlusDigitalOutputPort() - port = OPXPlusDigitalOutputPort(port=("con1", 2)) - assert port.port == ("con1", 2) + port = OPXPlusDigitalOutputPort("con1", 2) + assert port.controller_id == "con1" + assert port.port_id == 2 + assert port.port_tuple == ("con1", 2) assert port.port_type == "digital_output" - assert port.inverted == False - assert port.shareable == False + assert port.inverted is False + assert port.shareable is False + + assert port.to_dict() == { + "controller_id": "con1", + "port_id": 2, + } assert port.get_port_properties() == { "inverted": False, @@ -45,14 +52,21 @@ def test_opx_plus_digital_input_port(): with pytest.raises(TypeError): OPXPlusDigitalInputPort() - port = OPXPlusDigitalInputPort(port=("con1", 2)) - assert port.port == ("con1", 2) + port = OPXPlusDigitalInputPort("con1", 2) + assert port.controller_id == "con1" + assert port.port_id == 2 + assert port.port_tuple == ("con1", 2) assert port.port_type == "digital_input" assert port.deadtime == 4 assert port.polarity == "rising" assert port.threshold == 2.0 assert port.shareable == False + assert port.to_dict() == { + "controller_id": "con1", + "port_id": 2, + } + assert port.get_port_properties() == { "deadtime": 4, "polarity": "rising", @@ -83,14 +97,23 @@ def test_fem_digital_output_port(): with pytest.raises(TypeError): FEMDigitalOutputPort() - port = FEMDigitalOutputPort(port=("con1", 1, 2)) + port = FEMDigitalOutputPort("con1", 1, 2) + assert port.controller_id == "con1" + assert port.fem_id == 1 + assert port.port_id == 2 + assert port.port_tuple == ("con1", 1, 2) - assert port.port == ("con1", 1, 2) assert port.port_type == "digital_output" assert port.inverted == False assert port.shareable == False assert port.level == "LVTTL" + assert port.to_dict() == { + "controller_id": "con1", + "fem_id": 1, + "port_id": 2, + } + assert port.get_port_properties() == { "inverted": False, "shareable": False, diff --git a/tests/components/ports/test_lf_fem_analog_ports.py b/tests/components/ports/test_lf_fem_analog_ports.py index 089db71e..193ddb76 100644 --- a/tests/components/ports/test_lf_fem_analog_ports.py +++ b/tests/components/ports/test_lf_fem_analog_ports.py @@ -8,24 +8,30 @@ def test_lf_fem_analog_output_port(): with pytest.raises(TypeError): LFFEMAnalogOutputPort() - port = LFFEMAnalogOutputPort(port=("con1", 1, 2)) - assert port.port == ("con1", 1, 2) + port = LFFEMAnalogOutputPort("con1", 1, 2) + assert port.controller_id == "con1" + assert port.fem_id == 1 + assert port.port_id == 2 + assert port.port_tuple == ("con1", 1, 2) assert port.port_type == "analog_output" assert port.offset == None assert port.delay == 0 - assert port.crosstalk == {} - assert port.feedforward_filter == [] - assert port.feedback_filter == [] + assert port.crosstalk is None + assert port.feedforward_filter is None + assert port.feedback_filter is None assert port.shareable == False assert port.output_mode == "direct" assert port.sampling_rate == 1e9 assert port.upsampling_mode == "mw" + assert port.to_dict() == { + "controller_id": "con1", + "fem_id": 1, + "port_id": 2, + } + assert port.get_port_properties() == { "delay": 0, - "crosstalk": {}, - "feedforward_filter": [], - "feedback_filter": [], "shareable": False, "output_mode": "direct", "sampling_rate": 1e9, @@ -43,9 +49,6 @@ def test_lf_fem_analog_output_port(): "analog_outputs": { 2: { "delay": 0, - "crosstalk": {}, - "feedforward_filter": [], - "feedback_filter": [], "shareable": False, "output_mode": "direct", "sampling_rate": 1e9, @@ -61,9 +64,6 @@ def test_lf_fem_analog_output_port(): port.offset = 0.1 assert port.get_port_properties() == { "delay": 0, - "crosstalk": {}, - "feedforward_filter": [], - "feedback_filter": [], "shareable": False, "output_mode": "direct", "sampling_rate": 1e9, @@ -76,16 +76,25 @@ def test_lf_fem_analog_input_port(): with pytest.raises(TypeError): LFFEMAnalogInputPort() - port = LFFEMAnalogInputPort(port=("con1", 1, 2)) - assert port.port == ("con1", 1, 2) + port = LFFEMAnalogInputPort("con1", 1, 2) + assert port.controller_id == "con1" + assert port.fem_id == 1 + assert port.port_id == 2 + assert port.port_tuple == ("con1", 1, 2) + assert port.port_type == "analog_input" - assert port.offset == 0.0 + assert port.offset is None assert port.gain_db == 0 assert port.shareable == False assert port.sampling_rate == 1e9 + assert port.to_dict() == { + "controller_id": "con1", + "fem_id": 1, + "port_id": 2, + } + assert port.get_port_properties() == { - "offset": 0.0, "gain_db": 0, "shareable": False, "sampling_rate": 1e9, @@ -101,7 +110,6 @@ def test_lf_fem_analog_input_port(): 1: { "analog_inputs": { 2: { - "offset": 0.0, "gain_db": 0, "shareable": False, "sampling_rate": 1e9, diff --git a/tests/components/ports/test_mw_fem_analog_ports.py b/tests/components/ports/test_mw_fem_analog_ports.py index 3b031aee..aae30a92 100644 --- a/tests/components/ports/test_mw_fem_analog_ports.py +++ b/tests/components/ports/test_mw_fem_analog_ports.py @@ -9,35 +9,40 @@ def test_mw_fem_analog_output_port(): MWFEMAnalogOutputPort() with pytest.raises(TypeError): - port = MWFEMAnalogOutputPort(port=("con1", 1, 2)) + port = MWFEMAnalogOutputPort("con1", 1, 2) - port = MWFEMAnalogOutputPort(port=("con1", 1, 2), band=1) - assert port.port == ("con1", 1, 2) + with pytest.raises(ValueError): + port = MWFEMAnalogOutputPort("con1", 1, 2, band=1) + + port = MWFEMAnalogOutputPort("con1", 1, 2, band=1, upconverter_frequency=5e9) + assert port.controller_id == "con1" + assert port.fem_id == 1 + assert port.port_id == 2 + assert port.port_tuple == ("con1", 1, 2) assert port.port_type == "analog_output" assert port.band == 1 - assert port.upconverter_frequency is None + assert port.upconverter_frequency == 5e9 assert port.upconverters is None assert port.delay == 0 assert port.shareable == False assert port.sampling_rate == 1e9 assert port.full_scale_power_dbm == -11 - assert port.get_port_properties() == { + assert port.to_dict() == { + "controller_id": "con1", + "fem_id": 1, + "port_id": 2, "band": 1, - "delay": 0, - "shareable": False, - "sampling_rate": 1e9, - "full_scale_power_dbm": -11, + "upconverter_frequency": 5e9, } - port.upconverter_frequency = 5e9 assert port.get_port_properties() == { "band": 1, - "upconverter_frequency": 5e9, "delay": 0, "shareable": False, "sampling_rate": 1e9, "full_scale_power_dbm": -11, + "upconverter_frequency": 5e9, } cfg = {"controllers": {}} @@ -70,19 +75,28 @@ def test_mw_fem_analog_input_ports(): MWFEMAnalogInputPort() with pytest.raises(TypeError): - port = MWFEMAnalogInputPort(port=("con1", 1, 2)) + port = MWFEMAnalogInputPort("con1", 1, 2) - port = MWFEMAnalogInputPort( - port=("con1", 1, 2), band=1, downconverter_frequency=5e9 - ) + port = MWFEMAnalogInputPort("con1", 1, 2, band=1, downconverter_frequency=5e9) - assert port.port == ("con1", 1, 2) + assert port.controller_id == "con1" + assert port.fem_id == 1 + assert port.port_id == 2 + assert port.port_tuple == ("con1", 1, 2) assert port.port_type == "analog_input" assert port.band == 1 assert port.downconverter_frequency == 5e9 assert port.sampling_rate == 1e9 assert port.shareable == False + assert port.to_dict() == { + "controller_id": "con1", + "fem_id": 1, + "port_id": 2, + "band": 1, + "downconverter_frequency": 5e9, + } + assert port.get_port_properties() == { "band": 1, "downconverter_frequency": 5e9, diff --git a/tests/components/ports/test_opx_ports.py b/tests/components/ports/test_opx_ports.py index bc6a170f..7334f112 100644 --- a/tests/components/ports/test_opx_ports.py +++ b/tests/components/ports/test_opx_ports.py @@ -1,31 +1,31 @@ import pytest from quam.components.ports.analog_inputs import OPXPlusAnalogInputPort from quam.components.ports.analog_outputs import OPXPlusAnalogOutputPort -from quam.components.ports.digital_outputs import ( - OPXPlusDigitalOutputPort, -) def test_opx_plus_analog_output_port(): with pytest.raises(TypeError): OPXPlusAnalogOutputPort() - port = OPXPlusAnalogOutputPort(port=("con1", 2)) - assert port.port == ("con1", 2) + port = OPXPlusAnalogOutputPort("con1", 2) + assert port.controller_id == "con1" + assert port.port_id == 2 + assert port.port_tuple == ("con1", 2) assert port.port_type == "analog_output" - assert port.offset == 0.0 + assert port.offset is None assert port.delay == 0 - assert port.crosstalk == {} - assert port.feedforward_filter == [] - assert port.feedback_filter == [] + assert port.crosstalk is None + assert port.feedforward_filter is None + assert port.feedback_filter is None assert port.shareable == False assert port.get_port_properties() == { - "offset": 0.0, "delay": 0, - "crosstalk": {}, - "feedforward_filter": [], - "feedback_filter": [], + "shareable": False, + } + + assert port.get_port_properties() == { + "delay": 0, "shareable": False, } @@ -37,11 +37,7 @@ def test_opx_plus_analog_output_port(): "con1": { "analog_outputs": { 2: { - "offset": 0.0, "delay": 0, - "crosstalk": {}, - "feedforward_filter": [], - "feedback_filter": [], "shareable": False, } } @@ -54,15 +50,21 @@ def test_opx_plus_analog_input_port(): with pytest.raises(TypeError): OPXPlusAnalogInputPort() - port = OPXPlusAnalogInputPort(port=("con1", 2)) - assert port.port == ("con1", 2) + port = OPXPlusAnalogInputPort("con1", 2) + assert port.controller_id == "con1" + assert port.port_id == 2 + assert port.port_tuple == ("con1", 2) assert port.port_type == "analog_input" - assert port.offset == 0.0 + assert port.offset is None assert port.gain_db == 0 assert port.shareable == False assert port.get_port_properties() == { - "offset": 0.0, + "gain_db": 0, + "shareable": False, + } + + assert port.get_port_properties() == { "gain_db": 0, "shareable": False, } @@ -75,7 +77,6 @@ def test_opx_plus_analog_input_port(): "con1": { "analog_inputs": { 2: { - "offset": 0.0, "gain_db": 0, "shareable": False, } From 823a7887703aca4d0f7958361e2c97ee8410dfeb Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Mon, 1 Jul 2024 21:47:03 +0200 Subject: [PATCH 19/31] channel tuple fixes --- quam/components/channels.py | 36 ++++++++++++------------- quam/components/ports/analog_outputs.py | 4 +-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/quam/components/channels.py b/quam/components/channels.py index 452968de..0d2c88ae 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -106,11 +106,11 @@ def generate_element_config(self) -> Dict[str, int]: Contains "port", and optionally "delay", "buffer" if specified """ if isinstance(self.opx_output, DigitalOutputPort): - opx_output = self.opx_output.port + opx_output = self.opx_output.port_tuple else: - opx_output = self.opx_output + opx_output = tuple(self.opx_output) - digital_cfg: Dict[str, Any] = {"port": tuple(opx_output)} + digital_cfg: Dict[str, Any] = {"port": opx_output} if self.delay is not None: digital_cfg["delay"] = self.delay if self.buffer is not None: @@ -520,22 +520,22 @@ def apply_to_config(self, config: dict): opx_port = self.opx_output elif len(self.opx_output) == 2: opx_port = OPXPlusAnalogOutputPort( - port=self.opx_output, + *self.opx_output, offset=self.opx_output_offset, - feedforward_filter=list(self.filter_fir_taps), - feedback_filter=list(self.filter_iir_taps), + feedforward_filter=self.filter_fir_taps, + feedback_filter=self.filter_iir_taps, ) opx_port.apply_to_config(config) else: opx_port = LFFEMAnalogOutputPort( - port=self.opx_output, + *self.opx_output, offset=self.opx_output_offset, - feedforward_filter=list(self.filter_fir_taps), - feedback_filter=list(self.filter_iir_taps), + feedforward_filter=self.filter_fir_taps, + feedback_filter=self.filter_iir_taps, ) opx_port.apply_to_config(config) - element_config["singleInput"] = {"port": tuple(opx_port.port)} + element_config["singleInput"] = {"port": opx_port.port_tuple} @quam_dataclass @@ -582,16 +582,16 @@ def apply_to_config(self, config: dict): opx_port = self.opx_input elif len(self.opx_input) == 2: opx_port = OPXPlusAnalogInputPort( - port=self.opx_input, offset=self.opx_input_offset + *self.opx_input, offset=self.opx_input_offset ) opx_port.apply_to_config(config) else: opx_port = LFFEMAnalogInputPort( - port=self.opx_input, offset=self.opx_input_offset + *self.opx_input, offset=self.opx_input_offset ) opx_port.apply_to_config(config) - element_config["outputs"] = {"out1": tuple(opx_port.port)} + element_config["outputs"] = {"out1": opx_port.port_tuple} def measure( self, @@ -1100,16 +1100,16 @@ def apply_to_config(self, config: dict): opx_port = opx_input elif len(opx_input) == 2: opx_port = OPXPlusAnalogInputPort( - port=opx_input, offset=offset, gain_db=self.input_gain + *opx_input, offset=offset, gain_db=self.input_gain ) opx_port.apply_to_config(config) else: opx_port = LFFEMAnalogInputPort( - port=opx_input, offset=offset, gain_db=self.input_gain + *opx_input, offset=offset, gain_db=self.input_gain ) opx_port.apply_to_config(config) if not isinstance(self.frequency_converter_down, OctaveDownConverter): - element_cfg["outputs"][f"out{k}"] = tuple(opx_port.port) + element_cfg["outputs"][f"out{k}"] = opx_port.port_tuple def measure( self, @@ -1496,7 +1496,7 @@ def apply_to_config(self, config: Dict) -> None: super().apply_to_config(config) element_cfg = config["elements"][self.name] - element_cfg["MWInput"] = tuple(self.opx_output.port) + element_cfg["MWInput"] = self.opx_output.port_tuple element_cfg["upconverter"] = self.upconverter @@ -1508,7 +1508,7 @@ def apply_to_config(self, config: Dict) -> None: super().apply_to_config(config) element_cfg = config["elements"][self.name] - element_cfg["MWOutput"] = tuple(self.opx_input.port) + element_cfg["MWOutput"] = self.opx_input.port_tuple @quam_dataclass diff --git a/quam/components/ports/analog_outputs.py b/quam/components/ports/analog_outputs.py index 1e4a9c3b..f0c4c584 100644 --- a/quam/components/ports/analog_outputs.py +++ b/quam/components/ports/analog_outputs.py @@ -32,9 +32,9 @@ def get_port_properties(self): if self.crosstalk is not None: port_properties["crosstalk"] = self.crosstalk if self.feedforward_filter is not None: - port_properties["feedforward_filter"] = self.feedforward_filter + port_properties["feedforward_filter"] = list(self.feedforward_filter) if self.feedback_filter is not None: - port_properties["feedback_filter"] = self.feedback_filter + port_properties["feedback_filter"] = list(self.feedback_filter) if self.offset is not None: port_properties["offset"] = self.offset return port_properties From 7c7c05aff67f9cfe831b61b9dc3d59dd7288f17b Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Mon, 1 Jul 2024 21:51:05 +0200 Subject: [PATCH 20/31] minor fix to channels --- quam/components/channels.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/quam/components/channels.py b/quam/components/channels.py index 0d2c88ae..816799a1 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -141,11 +141,11 @@ def apply_to_config(self, config: dict) -> None: if len(self.opx_output) == 2: digital_output_port = OPXPlusDigitalOutputPort( - port=self.opx_output, shareable=self.shareable, inverted=self.inverted + *self.opx_output, shareable=self.shareable, inverted=self.inverted ) else: digital_output_port = FEMDigitalOutputPort( - port=self.opx_output, shareable=self.shareable, inverted=self.inverted + *self.opx_output, shareable=self.shareable, inverted=self.inverted ) digital_output_port.apply_to_config(config) @@ -1011,10 +1011,10 @@ def apply_to_config(self, config: dict): if isinstance(opx_output, LFAnalogOutputPort): opx_port = opx_output elif len(opx_output) == 2: - opx_port = OPXPlusAnalogOutputPort(port=opx_output, offset=offset) + opx_port = OPXPlusAnalogOutputPort(*opx_output, offset=offset) opx_port.apply_to_config(config) else: - opx_port = LFFEMAnalogOutputPort(port=opx_output, offset=offset) + opx_port = LFFEMAnalogOutputPort(*opx_output, offset=offset) opx_port.apply_to_config(config) From f4d4e74b24a6343bb38ae209d67695a0406e8d0a Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 2 Jul 2024 06:59:10 +0200 Subject: [PATCH 21/31] fixing tests --- quam/components/channels.py | 20 +++++++++++-------- .../channels/test_digital_channel.py | 2 +- .../channels/test_in_IQ_out_single_channel.py | 7 +++---- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/quam/components/channels.py b/quam/components/channels.py index 816799a1..671f1a74 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -139,13 +139,15 @@ def apply_to_config(self, config: dict) -> None: ) return + shareable = self.shareable if self.shareable is not None else False + inverted = self.inverted if self.inverted is not None else False if len(self.opx_output) == 2: digital_output_port = OPXPlusDigitalOutputPort( - *self.opx_output, shareable=self.shareable, inverted=self.inverted + *self.opx_output, shareable=shareable, inverted=inverted ) else: digital_output_port = FEMDigitalOutputPort( - *self.opx_output, shareable=self.shareable, inverted=self.inverted + *self.opx_output, shareable=shareable, inverted=inverted ) digital_output_port.apply_to_config(config) @@ -964,8 +966,6 @@ def apply_to_config(self, config: dict): # Add pulses & waveforms super().apply_to_config(config) - opx_outputs = {"I": self.opx_output_I, "Q": self.opx_output_Q} - if str_ref.is_reference(self.name): raise AttributeError( f"Channel {self.get_reference()} cannot be added to the config because" @@ -997,7 +997,8 @@ def apply_to_config(self, config: dict): f"reference: {self.frequency_converter_up}" ) else: - element_cfg["mixInputs"] = {**opx_ports} + + element_cfg["mixInputs"] = {} # To be filled in next section if self.mixer is not None: element_cfg["mixInputs"]["mixer"] = self.mixer.name if self.local_oscillator is not None: @@ -1007,7 +1008,7 @@ def apply_to_config(self, config: dict): opx_outputs = [self.opx_output_I, self.opx_output_Q] offsets = [self.opx_output_offset_I, self.opx_output_offset_Q] - for opx_output, offset in zip(opx_outputs, offsets): + for I_or_Q, opx_output, offset in zip("IQ", opx_outputs, offsets): if isinstance(opx_output, LFAnalogOutputPort): opx_port = opx_output elif len(opx_output) == 2: @@ -1017,6 +1018,8 @@ def apply_to_config(self, config: dict): opx_port = LFFEMAnalogOutputPort(*opx_output, offset=offset) opx_port.apply_to_config(config) + element_cfg["mixInputs"][I_or_Q] = opx_port.port_tuple + @quam_dataclass class InIQChannel(Channel): @@ -1095,17 +1098,18 @@ def apply_to_config(self, config: dict): opx_inputs = [self.opx_input_I, self.opx_input_Q] offsets = [self.opx_input_offset_I, self.opx_input_offset_Q] + input_gain = int(self.input_gain if self.input_gain is not None else 0) for k, (opx_input, offset) in enumerate(zip(opx_inputs, offsets), start=1): if isinstance(opx_input, LFAnalogInputPort): opx_port = opx_input elif len(opx_input) == 2: opx_port = OPXPlusAnalogInputPort( - *opx_input, offset=offset, gain_db=self.input_gain + *opx_input, offset=offset, gain_db=input_gain ) opx_port.apply_to_config(config) else: opx_port = LFFEMAnalogInputPort( - *opx_input, offset=offset, gain_db=self.input_gain + *opx_input, offset=offset, gain_db=input_gain ) opx_port.apply_to_config(config) if not isinstance(self.frequency_converter_down, OctaveDownConverter): diff --git a/tests/components/channels/test_digital_channel.py b/tests/components/channels/test_digital_channel.py index 281e4622..652d4049 100644 --- a/tests/components/channels/test_digital_channel.py +++ b/tests/components/channels/test_digital_channel.py @@ -18,7 +18,7 @@ def test_digital_only_channel(qua_config): quam = QuamTest(channel=channel) cfg = quam.generate_config() - qua_config["controllers"] = {"con1": {"digital_outputs": {1: {}}}} + qua_config["controllers"] = {"con1": {"digital_outputs": {1: {"inverted": False, "shareable": False}}}} qua_config["elements"] = { "channel": {"digitalInputs": {"1": {"port": ("con1", 1)}}, "operations": {}} } diff --git a/tests/components/channels/test_in_IQ_out_single_channel.py b/tests/components/channels/test_in_IQ_out_single_channel.py index e266839f..cecf7824 100644 --- a/tests/components/channels/test_in_IQ_out_single_channel.py +++ b/tests/components/channels/test_in_IQ_out_single_channel.py @@ -40,11 +40,10 @@ def test_generate_config(qua_config): assert qua_config["controllers"] == { "con1": { "analog_inputs": { - 1: {}, - 2: {}, + 1: {"gain_db": 0, "shareable": False}, + 2: {"gain_db": 0, "shareable": False}, }, - "analog_outputs": {1: {}}, - "digital_outputs": {}, + "analog_outputs": {1: {"delay": 0, "shareable": False}}, } } From 0dae7469b93235a5f15948ba4e7299c872219c7d Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 2 Jul 2024 07:41:20 +0200 Subject: [PATCH 22/31] fully working tests --- quam/components/channels.py | 5 ++- quam/components/ports/analog_inputs.py | 2 + quam/components/ports/analog_outputs.py | 3 ++ quam/components/ports/base_ports.py | 15 +++++-- .../channels/test_in_out_IQ_channel.py | 18 ++++----- .../channels/test_in_out_single_channel.py | 5 +-- .../channels/test_in_single_channel.py | 4 +- .../channels/test_in_single_out_IQ_channel.py | 8 ++-- .../channels/test_single_channel.py | 19 +++++---- .../ports/test_lf_fem_analog_ports.py | 6 ++- .../ports/test_mw_fem_analog_ports.py | 2 + .../superconducting_qubits/test_transmon.py | 39 ++++++++++++++----- 12 files changed, 81 insertions(+), 45 deletions(-) diff --git a/quam/components/channels.py b/quam/components/channels.py index 671f1a74..aff44088 100644 --- a/quam/components/channels.py +++ b/quam/components/channels.py @@ -1018,7 +1018,8 @@ def apply_to_config(self, config: dict): opx_port = LFFEMAnalogOutputPort(*opx_output, offset=offset) opx_port.apply_to_config(config) - element_cfg["mixInputs"][I_or_Q] = opx_port.port_tuple + if "mixInputs" in element_cfg: + element_cfg["mixInputs"][I_or_Q] = opx_port.port_tuple @quam_dataclass @@ -1054,7 +1055,7 @@ class InIQChannel(Channel): opx_input_offset_I: float = None opx_input_offset_Q: float = None - input_gain: Optional[float] = None + input_gain: Optional[int] = None frequency_converter_down: BaseFrequencyConverter = None diff --git a/quam/components/ports/analog_inputs.py b/quam/components/ports/analog_inputs.py index b7d1675a..10999928 100644 --- a/quam/components/ports/analog_inputs.py +++ b/quam/components/ports/analog_inputs.py @@ -15,6 +15,7 @@ @quam_dataclass class LFAnalogInputPort(BasePort, ABC): + fem_type: ClassVar[str] = "LF" port_type: ClassVar[str] = "analog_input" offset: Optional[float] = None @@ -48,6 +49,7 @@ def get_port_properties(self) -> Dict[str, Any]: @quam_dataclass class MWFEMAnalogInputPort(FEMPort): + fem_type: ClassVar[str] = "MW" port_type: ClassVar[str] = "analog_input" band: int diff --git a/quam/components/ports/analog_outputs.py b/quam/components/ports/analog_outputs.py index f0c4c584..4d1954ee 100644 --- a/quam/components/ports/analog_outputs.py +++ b/quam/components/ports/analog_outputs.py @@ -15,6 +15,7 @@ @quam_dataclass class LFAnalogOutputPort(BasePort, ABC): + fem_type: ClassVar[str] = "LF" port_type: ClassVar[str] = "analog_output" offset: Optional[float] = None @@ -47,6 +48,7 @@ class OPXPlusAnalogOutputPort(LFAnalogOutputPort, OPXPlusPort): @quam_dataclass class LFFEMAnalogOutputPort(LFAnalogOutputPort, FEMPort): + fem_type: ClassVar[str] = "LF" sampling_rate: float = 1e9 # Either 1e9 or 2e9 upsampling_mode: Literal["mw", "pulse"] = "mw" output_mode: Literal["direct", "amplified"] = "direct" @@ -61,6 +63,7 @@ def get_port_properties(self) -> Dict[str, Any]: @quam_dataclass class MWFEMAnalogOutputPort(FEMPort): + fem_type: ClassVar[str] = "MW" port_type: ClassVar[str] = "analog_output" band: int diff --git a/quam/components/ports/base_ports.py b/quam/components/ports/base_ports.py index cef03c05..f9756d9e 100644 --- a/quam/components/ports/base_ports.py +++ b/quam/components/ports/base_ports.py @@ -33,9 +33,9 @@ def _update_port_config(self, port_config, port_properties): if key in port_config and value != port_config[key]: warnings.warn( f"Error generating QUA config: Controller {self.port_type} " - f"port {self.port_tuple} already has entry for {key}. This likely " - f"means that the port is being configured multiple times. " - f"Overwriting {port_config['key']} → {value}." + f"port {self.port_tuple} already has entry for {key}. This " + f"likely means that the port is being configured multiple " + f"times. Overwriting {port_config[key]} → {value}." ) except Exception: pass @@ -80,6 +80,7 @@ def get_port_config( @quam_dataclass class FEMPort(BasePort, ABC): + fem_type: ClassVar[str] controller_id: Union[str, int] fem_id: int port_id: int @@ -113,6 +114,14 @@ def get_port_config( controller_cfg = config["controllers"].setdefault(self.controller_id, {}) fems_cfg = controller_cfg.setdefault("fems", {}) fem_cfg = fems_cfg.setdefault(self.fem_id, {}) + if hasattr(self, "fem_type"): + if fem_cfg.get("type", self.fem_type) != self.fem_type: + raise ValueError( + f"Error generating config: FEM {self.fem_id} is not of type " + f"{self.fem_type}" + ) + fem_cfg["type"] = self.fem_type + ports_cfg = fem_cfg.setdefault(f"{self.port_type}s", {}) port_cfg = ports_cfg.setdefault(self.port_id, {}) return port_cfg diff --git a/tests/components/channels/test_in_out_IQ_channel.py b/tests/components/channels/test_in_out_IQ_channel.py index 65caa52f..54088de9 100644 --- a/tests/components/channels/test_in_out_IQ_channel.py +++ b/tests/components/channels/test_in_out_IQ_channel.py @@ -64,14 +64,13 @@ def test_empty_in_out_IQ_channel(): "controllers": { "con1": { "analog_inputs": { - 3: {}, - 4: {}, + 3: {"gain_db": 0, "shareable": False}, + 4: {"gain_db": 0, "shareable": False}, }, "analog_outputs": { - 1: {}, - 2: {}, + 1: {"delay": 0, "shareable": False}, + 2: {"delay": 0, "shareable": False}, }, - "digital_outputs": {}, } }, "elements": { @@ -159,14 +158,13 @@ def test_readout_resonator_with_readout(): "controllers": { "con1": { "analog_inputs": { - 3: {}, - 4: {}, + 3: {"gain_db": 0, "shareable": False}, + 4: {"gain_db": 0, "shareable": False}, }, "analog_outputs": { - 1: {}, - 2: {}, + 1: {"delay": 0, "shareable": False}, + 2: {"delay": 0, "shareable": False}, }, - "digital_outputs": {}, } }, "elements": { diff --git a/tests/components/channels/test_in_out_single_channel.py b/tests/components/channels/test_in_out_single_channel.py index cff1eb0b..5b88d730 100644 --- a/tests/components/channels/test_in_out_single_channel.py +++ b/tests/components/channels/test_in_out_single_channel.py @@ -30,9 +30,8 @@ def test_in_out_single_channel(): expected_cfg = { "controllers": { "con1": { - "analog_inputs": {2: {}}, - "analog_outputs": {1: {}}, - "digital_outputs": {}, + "analog_inputs": {2: {"gain_db": 0, "shareable": False}}, + "analog_outputs": {1: {"delay": 0, "shareable": False}}, } }, "elements": { diff --git a/tests/components/channels/test_in_single_channel.py b/tests/components/channels/test_in_single_channel.py index b7b48b50..c0c3854e 100644 --- a/tests/components/channels/test_in_single_channel.py +++ b/tests/components/channels/test_in_single_channel.py @@ -23,10 +23,8 @@ def test_generate_config(qua_config): assert qua_config["controllers"] == { "con1": { "analog_inputs": { - 1: {}, + 1: {"gain_db": 0, "shareable": False}, }, - "analog_outputs": {}, - "digital_outputs": {}, } } diff --git a/tests/components/channels/test_in_single_out_IQ_channel.py b/tests/components/channels/test_in_single_out_IQ_channel.py index b0ac736c..13df4390 100644 --- a/tests/components/channels/test_in_single_out_IQ_channel.py +++ b/tests/components/channels/test_in_single_out_IQ_channel.py @@ -39,10 +39,12 @@ def test_generate_config(qua_config): assert qua_config["controllers"] == { "con1": { "analog_inputs": { - 1: {}, + 1: {"gain_db": 0, "shareable": False}, + }, + "analog_outputs": { + 1: {"delay": 0, "shareable": False}, + 2: {"delay": 0, "shareable": False}, }, - "analog_outputs": {1: {}, 2: {}}, - "digital_outputs": {}, } } diff --git a/tests/components/channels/test_single_channel.py b/tests/components/channels/test_single_channel.py index a9f9433a..f3caccf7 100644 --- a/tests/components/channels/test_single_channel.py +++ b/tests/components/channels/test_single_channel.py @@ -33,9 +33,7 @@ def test_single_channel_offset(bare_cfg): expected_cfg = { "controllers": { "con1": { - "analog_inputs": {}, - "digital_outputs": {}, - "analog_outputs": {1: {}}, + "analog_outputs": {1: {"delay": 0, "shareable": False}}, } }, "elements": { @@ -61,21 +59,24 @@ def test_single_channel_differing_offsets(bare_cfg): cfg = deepcopy(bare_cfg) channel1.apply_to_config(cfg) channel2.apply_to_config(cfg) - assert cfg["controllers"]["con1"]["analog_outputs"][1] == {} + assert cfg["controllers"]["con1"]["analog_outputs"][1] == { + "delay": 0, + "shareable": False, + } channel1.opx_output_offset = 0.1 cfg = deepcopy(bare_cfg) channel1.apply_to_config(cfg) channel2.apply_to_config(cfg) - assert cfg["controllers"]["con1"]["analog_outputs"][1] == {"offset": 0.1} + assert cfg["controllers"]["con1"]["analog_outputs"][1]["offset"] == 0.1 channel2.opx_output_offset = 0.1 cfg = deepcopy(bare_cfg) channel1.apply_to_config(cfg) channel2.apply_to_config(cfg) - assert cfg["controllers"]["con1"]["analog_outputs"][1] == {"offset": 0.1} + assert cfg["controllers"]["con1"]["analog_outputs"][1]["offset"] == 0.1 channel2.opx_output_offset = 0.2 @@ -88,7 +89,7 @@ def test_single_channel_differing_offsets(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} + assert cfg["controllers"]["con1"]["analog_outputs"][1]["offset"] == 0.1 + 0.5e-4 def test_single_channel_offset_quam(qua_config): @@ -103,9 +104,7 @@ class QuamTest(QuamRoot): qua_config["controllers"] = { "con1": { - "analog_inputs": {}, - "digital_outputs": {}, - "analog_outputs": {1: {"offset": 0.0}}, + "analog_outputs": {1: {"delay": 0, "shareable": False, "offset": 0.0}}, } } qua_config["elements"] = { diff --git a/tests/components/ports/test_lf_fem_analog_ports.py b/tests/components/ports/test_lf_fem_analog_ports.py index 193ddb76..8d9cbfad 100644 --- a/tests/components/ports/test_lf_fem_analog_ports.py +++ b/tests/components/ports/test_lf_fem_analog_ports.py @@ -46,6 +46,7 @@ def test_lf_fem_analog_output_port(): "con1": { "fems": { 1: { + "type": "LF", "analog_outputs": { 2: { "delay": 0, @@ -54,7 +55,7 @@ def test_lf_fem_analog_output_port(): "sampling_rate": 1e9, "upsampling_mode": "mw", } - } + }, } } } @@ -108,13 +109,14 @@ def test_lf_fem_analog_input_port(): "con1": { "fems": { 1: { + "type": "LF", "analog_inputs": { 2: { "gain_db": 0, "shareable": False, "sampling_rate": 1e9, } - } + }, } } } diff --git a/tests/components/ports/test_mw_fem_analog_ports.py b/tests/components/ports/test_mw_fem_analog_ports.py index aae30a92..46043843 100644 --- a/tests/components/ports/test_mw_fem_analog_ports.py +++ b/tests/components/ports/test_mw_fem_analog_ports.py @@ -53,6 +53,7 @@ def test_mw_fem_analog_output_port(): "con1": { "fems": { 1: { + "type": "MW", "analog_outputs": { 2: { "band": 1, @@ -112,6 +113,7 @@ def test_mw_fem_analog_input_ports(): "con1": { "fems": { 1: { + "type": "MW", "analog_inputs": { 2: { "band": 1, diff --git a/tests/examples/superconducting_qubits/test_transmon.py b/tests/examples/superconducting_qubits/test_transmon.py index 26e3cf70..4689935c 100644 --- a/tests/examples/superconducting_qubits/test_transmon.py +++ b/tests/examples/superconducting_qubits/test_transmon.py @@ -63,9 +63,10 @@ def test_transmon_xy(): }, "controllers": { "con1": { - "analog_outputs": {1: {}, 2: {}}, - "digital_outputs": {}, - "analog_inputs": {}, + "analog_outputs": { + 1: {"delay": 0, "shareable": False}, + 2: {"delay": 0, "shareable": False}, + }, } }, } @@ -121,9 +122,22 @@ def test_transmon_xy_opx1000(): "fems": { 2: { "type": "LF", - "analog_outputs": {1: {}, 2: {}}, - "digital_outputs": {}, - "analog_inputs": {}, + "analog_outputs": { + 1: { + "delay": 0, + "shareable": False, + "sampling_rate": 1e9, + "upsampling_mode": "mw", + "output_mode": "direct", + }, + 2: { + "delay": 0, + "shareable": False, + "sampling_rate": 1e9, + "upsampling_mode": "mw", + "output_mode": "direct", + }, + }, } } } @@ -182,9 +196,16 @@ def test_transmon_add_pulse(): assert config == { "controllers": { "con1": { - "analog_outputs": {1: {}, 2: {}}, - "digital_outputs": {}, - "analog_inputs": {}, + "analog_outputs": { + 1: { + "delay": 0, + "shareable": False, + }, + 2: { + "delay": 0, + "shareable": False, + }, + }, } }, "elements": { From d2805722823e4ef4550124dfd30225af57dbe1f5 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 2 Jul 2024 10:05:34 +0200 Subject: [PATCH 23/31] Added channel tests --- README.md | 2 +- tests/components/channels/test_IQ_channel.py | 67 +++++++++ .../channels/test_digital_channel.py | 31 ++++- .../channels/test_in_IQ_out_single_channel.py | 37 +++++ .../channels/test_in_out_IQ_channel.py | 128 ++++++++++++++++++ .../channels/test_in_out_single_channel.py | 36 +++++ .../channels/test_in_single_channel.py | 27 ++++ .../channels/test_in_single_out_IQ_channel.py | 43 ++++++ .../channels/test_single_channel.py | 44 ++++++ 9 files changed, 413 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fd8589b0..6621ae5b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Explore detailed documentation and get started with QuAM here: [QuAM Documentati - **State Management**: Features robust tools for saving and loading your quantum states, promoting reproducibility and consistency. ## Installation -To install QuAM, first ensure you have Python ≥ 3.8 installed on your system. +To install QuAM, first ensure you have 3.8 ≤ Python ≤ 3.11 installed on your system. Then run the following command: ```bash diff --git a/tests/components/channels/test_IQ_channel.py b/tests/components/channels/test_IQ_channel.py index 3c3d06f2..fd1892bd 100644 --- a/tests/components/channels/test_IQ_channel.py +++ b/tests/components/channels/test_IQ_channel.py @@ -1,5 +1,6 @@ import pytest from quam.components import IQChannel +from quam.components.ports.analog_outputs import OPXPlusAnalogOutputPort def test_IQ_channel_set_dc_offset(mocker): @@ -88,3 +89,69 @@ def test_IQ_channel_inferred_LO_frequency(): channel.intermediate_frequency = None with pytest.raises(AttributeError): channel.inferred_LO_frequency + + +def test_generate_config(qua_config): + channel = IQChannel( + id="out_channel", + opx_output_I=("con1", 1), + opx_output_Q=("con1", 2), + frequency_converter_up=None, + ) + + channel.apply_to_config(qua_config) + channel.opx_output_I.apply_to_config(qua_config) + channel.opx_output_Q.apply_to_config(qua_config) + + assert qua_config["controllers"] == { + "con1": { + "analog_outputs": { + 1: {"delay": 0, "shareable": False}, + 2: {"delay": 0, "shareable": False}, + }, + } + } + + assert qua_config["elements"] == { + "out_channel": { + "intermediate_frequency": 0.0, + "mixInputs": { + "I": ("con1", 1), + "Q": ("con1", 2), + }, + "operations": {}, + } + } + + +def test_generate_config_ports(qua_config): + channel = IQChannel( + id="out_channel", + opx_output_I=OPXPlusAnalogOutputPort("con1", 1), + opx_output_Q=OPXPlusAnalogOutputPort("con1", 2), + frequency_converter_up=None, + ) + + channel.apply_to_config(qua_config) + channel.opx_output_I.apply_to_config(qua_config) + channel.opx_output_Q.apply_to_config(qua_config) + + assert qua_config["controllers"] == { + "con1": { + "analog_outputs": { + 1: {"delay": 0, "shareable": False}, + 2: {"delay": 0, "shareable": False}, + }, + } + } + + assert qua_config["elements"] == { + "out_channel": { + "intermediate_frequency": 0.0, + "mixInputs": { + "I": ("con1", 1), + "Q": ("con1", 2), + }, + "operations": {}, + } + } diff --git a/tests/components/channels/test_digital_channel.py b/tests/components/channels/test_digital_channel.py index 652d4049..2b3ed805 100644 --- a/tests/components/channels/test_digital_channel.py +++ b/tests/components/channels/test_digital_channel.py @@ -1,5 +1,6 @@ from copy import deepcopy from quam.components import Channel, DigitalOutputChannel, pulses +from quam.components.ports.digital_outputs import OPXPlusDigitalOutputPort from quam.core import QuamRoot, quam_dataclass from quam.core.quam_instantiation import instantiate_quam_class @@ -18,7 +19,9 @@ def test_digital_only_channel(qua_config): quam = QuamTest(channel=channel) cfg = quam.generate_config() - qua_config["controllers"] = {"con1": {"digital_outputs": {1: {"inverted": False, "shareable": False}}}} + qua_config["controllers"] = { + "con1": {"digital_outputs": {1: {"inverted": False, "shareable": False}}} + } qua_config["elements"] = { "channel": {"digitalInputs": {"1": {"port": ("con1", 1)}}, "operations": {}} } @@ -26,6 +29,32 @@ def test_digital_only_channel(qua_config): assert cfg == qua_config +def test_digital_only_channel_with_port(qua_config): + + channel = Channel( + id="channel", + digital_outputs={ + "1": DigitalOutputChannel( + opx_output=OPXPlusDigitalOutputPort( + "con1", 2, inverted=True, shareable=True + ) + ) + }, + ) + + quam = QuamTest(channel=channel) + cfg = quam.generate_config() + + qua_config["controllers"] = { + "con1": {"digital_outputs": {2: {"inverted": True, "shareable": True}}} + } + qua_config["elements"] = { + "channel": {"digitalInputs": {"1": {"port": ("con1", 2)}}, "operations": {}} + } + + assert cfg == qua_config + + def test_digital_only_pulse(qua_config): channel = Channel( id="channel", diff --git a/tests/components/channels/test_in_IQ_out_single_channel.py b/tests/components/channels/test_in_IQ_out_single_channel.py index cecf7824..3090a922 100644 --- a/tests/components/channels/test_in_IQ_out_single_channel.py +++ b/tests/components/channels/test_in_IQ_out_single_channel.py @@ -1,4 +1,6 @@ from quam.components.channels import InIQOutSingleChannel +from quam.components.ports.analog_inputs import OPXPlusAnalogInputPort +from quam.components.ports.analog_outputs import OPXPlusAnalogOutputPort from quam.utils.dataclass import get_dataclass_attr_annotations @@ -56,3 +58,38 @@ def test_generate_config(qua_config): "time_of_flight": 24, } } + + +def test_generate_config_ports(qua_config): + channel = InIQOutSingleChannel( + id="in_out_channel", + opx_output=OPXPlusAnalogOutputPort("con1", 1), + opx_input_I=OPXPlusAnalogInputPort("con1", 1), + opx_input_Q=OPXPlusAnalogInputPort("con1", 2), + frequency_converter_down=None, + ) + + channel.apply_to_config(qua_config) + channel.opx_output.apply_to_config(qua_config) + channel.opx_input_I.apply_to_config(qua_config) + channel.opx_input_Q.apply_to_config(qua_config) + + assert qua_config["controllers"] == { + "con1": { + "analog_inputs": { + 1: {"gain_db": 0, "shareable": False}, + 2: {"gain_db": 0, "shareable": False}, + }, + "analog_outputs": {1: {"delay": 0, "shareable": False}}, + } + } + + assert qua_config["elements"] == { + "in_out_channel": { + "operations": {}, + "outputs": {"out1": ("con1", 1), "out2": ("con1", 2)}, + "singleInput": {"port": ("con1", 1)}, + "smearing": 0, + "time_of_flight": 24, + } + } diff --git a/tests/components/channels/test_in_out_IQ_channel.py b/tests/components/channels/test_in_out_IQ_channel.py index 54088de9..da0ddf89 100644 --- a/tests/components/channels/test_in_out_IQ_channel.py +++ b/tests/components/channels/test_in_out_IQ_channel.py @@ -1,5 +1,7 @@ import pytest from quam.components import * +from quam.components.ports.analog_inputs import OPXPlusAnalogInputPort +from quam.components.ports.analog_outputs import OPXPlusAnalogOutputPort def test_empty_in_out_IQ_channel(): @@ -254,3 +256,129 @@ def test_channel_measure(mocker): mocker.patch("quam.components.channels.measure", return_value=1) result = readout_resonator.measure("readout") assert result == (1, 1) + + +def test_empty_in_out_IQ_channel_ports(): + readout_resonator = InOutIQChannel( + frequency_converter_up=FrequencyConverter( + mixer=Mixer(), local_oscillator=LocalOscillator() + ), + opx_output_I=OPXPlusAnalogOutputPort("con1", 1), + opx_output_Q=OPXPlusAnalogOutputPort("con1", 2), + opx_input_I=OPXPlusAnalogInputPort("con1", 3), + opx_input_Q=OPXPlusAnalogInputPort("con1", 4), + intermediate_frequency=100e6, + ) + + assert isinstance(readout_resonator.frequency_converter_up.mixer, Mixer) + + assert isinstance(readout_resonator.local_oscillator, LocalOscillator) + assert ( + readout_resonator.frequency_converter_up.local_oscillator + is readout_resonator.local_oscillator + ) + + mixer = readout_resonator.frequency_converter_up.mixer + assert mixer.intermediate_frequency == 100e6 + + assert mixer.local_oscillator_frequency is None + readout_resonator.local_oscillator.frequency = 5e9 + assert mixer.local_oscillator_frequency == 5e9 + + with pytest.raises(AttributeError): + mixer.name + assert readout_resonator.id is None + with pytest.raises(AttributeError): + readout_resonator.name + + readout_resonator.id = 1 + + d = readout_resonator.to_dict() + assert d == { + "frequency_converter_up": { + "__class__": "quam.components.hardware.FrequencyConverter", + "mixer": {}, + "local_oscillator": {"frequency": 5000000000.0}, + }, + "opx_output_I": { + "controller_id": "con1", + "port_id": 1, + "__class__": "quam.components.ports.analog_outputs.OPXPlusAnalogOutputPort", + }, + "opx_output_Q": { + "controller_id": "con1", + "port_id": 2, + "__class__": "quam.components.ports.analog_outputs.OPXPlusAnalogOutputPort", + }, + "opx_input_I": { + "controller_id": "con1", + "port_id": 3, + "__class__": "quam.components.ports.analog_inputs.OPXPlusAnalogInputPort", + }, + "opx_input_Q": { + "controller_id": "con1", + "port_id": 4, + "__class__": "quam.components.ports.analog_inputs.OPXPlusAnalogInputPort", + }, + "intermediate_frequency": 100000000.0, + "id": 1, + } + + bare_cfg = { + "controllers": {}, + "elements": {}, + "pulses": {}, + "waveforms": {}, + "integration_weights": {}, + } + cfg = bare_cfg.copy() + expected_cfg = { + "controllers": { + "con1": { + "analog_inputs": { + 3: {"gain_db": 0, "shareable": False}, + 4: {"gain_db": 0, "shareable": False}, + }, + "analog_outputs": { + 1: {"delay": 0, "shareable": False}, + 2: {"delay": 0, "shareable": False}, + }, + } + }, + "elements": { + "IQ1": { + "intermediate_frequency": 100000000.0, + "mixInputs": { + "I": ("con1", 1), + "Q": ("con1", 2), + "lo_frequency": 5000000000.0, + "mixer": "IQ1.mixer", + }, + "operations": {}, + "outputs": {"out1": ("con1", 3), "out2": ("con1", 4)}, + "smearing": 0, + "time_of_flight": 24, + } + }, + "pulses": {}, + "waveforms": {}, + "integration_weights": {}, + } + readout_resonator.apply_to_config(cfg) + readout_resonator.opx_output_I.apply_to_config(cfg) + readout_resonator.opx_output_Q.apply_to_config(cfg) + readout_resonator.opx_input_I.apply_to_config(cfg) + readout_resonator.opx_input_Q.apply_to_config(cfg) + assert cfg == expected_cfg + + cfg = bare_cfg.copy() + readout_resonator._default_label = "res" + readout_resonator.apply_to_config(cfg) + expected_cfg["elements"]["res1"] = expected_cfg["elements"].pop("IQ1") + expected_cfg["elements"]["res1"]["mixInputs"]["mixer"] = "IQ1.mixer" + + cfg = bare_cfg.copy() + readout_resonator.id = "resonator_1" + readout_resonator.apply_to_config(cfg) + expected_cfg["elements"]["resonator_1"] = expected_cfg["elements"].pop("res1") + expected_cfg["elements"]["resonator_1"]["mixInputs"]["mixer"] = "resonator_1.mixer" diff --git a/tests/components/channels/test_in_out_single_channel.py b/tests/components/channels/test_in_out_single_channel.py index 5b88d730..ec106727 100644 --- a/tests/components/channels/test_in_out_single_channel.py +++ b/tests/components/channels/test_in_out_single_channel.py @@ -1,5 +1,7 @@ import pytest from quam.components import * +from quam.components.ports.analog_inputs import OPXPlusAnalogInputPort +from quam.components.ports.analog_outputs import OPXPlusAnalogOutputPort def test_in_out_single_channel_empty_error(): @@ -46,3 +48,37 @@ def test_in_out_single_channel(): } assert cfg == expected_cfg + + +def test_in_out_single_channel_ports(): + channel = InOutSingleChannel( + id=1, + opx_output=OPXPlusAnalogOutputPort("con1", 1), + opx_input=OPXPlusAnalogInputPort("con1", 2), + ) + + cfg = {"controllers": {}, "elements": {}} + + channel.apply_to_config(cfg) + channel.opx_output.apply_to_config(cfg) + channel.opx_input.apply_to_config(cfg) + + expected_cfg = { + "controllers": { + "con1": { + "analog_inputs": {2: {"gain_db": 0, "shareable": False}}, + "analog_outputs": {1: {"delay": 0, "shareable": False}}, + } + }, + "elements": { + "ch1": { + "operations": {}, + "outputs": {"out1": ("con1", 2)}, + "singleInput": {"port": ("con1", 1)}, + "smearing": 0, + "time_of_flight": 24, + } + }, + } + + assert cfg == expected_cfg diff --git a/tests/components/channels/test_in_single_channel.py b/tests/components/channels/test_in_single_channel.py index c0c3854e..ff2be564 100644 --- a/tests/components/channels/test_in_single_channel.py +++ b/tests/components/channels/test_in_single_channel.py @@ -1,4 +1,5 @@ from quam.components.channels import InSingleChannel +from quam.components.ports.analog_inputs import OPXPlusAnalogInputPort from quam.utils.dataclass import get_dataclass_attr_annotations @@ -36,3 +37,29 @@ def test_generate_config(qua_config): "time_of_flight": 24, } } + + +def test_generate_config_ports(qua_config): + channel = InSingleChannel( + id="input_channel", opx_input=OPXPlusAnalogInputPort("con1", 1) + ) + + channel.apply_to_config(qua_config) + channel.opx_input.apply_to_config(qua_config) + + assert qua_config["controllers"] == { + "con1": { + "analog_inputs": { + 1: {"gain_db": 0, "shareable": False}, + }, + } + } + + assert qua_config["elements"] == { + "input_channel": { + "operations": {}, + "outputs": {"out1": ("con1", 1)}, + "smearing": 0, + "time_of_flight": 24, + } + } diff --git a/tests/components/channels/test_in_single_out_IQ_channel.py b/tests/components/channels/test_in_single_out_IQ_channel.py index 13df4390..a0b92211 100644 --- a/tests/components/channels/test_in_single_out_IQ_channel.py +++ b/tests/components/channels/test_in_single_out_IQ_channel.py @@ -1,4 +1,6 @@ from quam.components.channels import InSingleOutIQChannel +from quam.components.ports.analog_inputs import OPXPlusAnalogInputPort +from quam.components.ports.analog_outputs import OPXPlusAnalogOutputPort from quam.utils.dataclass import get_dataclass_attr_annotations @@ -61,3 +63,44 @@ def test_generate_config(qua_config): "time_of_flight": 24, } } + + +def test_generate_config(qua_config): + channel = InSingleOutIQChannel( + id="in_out_channel", + opx_input=OPXPlusAnalogInputPort("con1", 1), + opx_output_I=OPXPlusAnalogOutputPort("con1", 1), + opx_output_Q=OPXPlusAnalogOutputPort("con1", 2), + frequency_converter_up=None, + ) + + channel.apply_to_config(qua_config) + channel.opx_input.apply_to_config(qua_config) + channel.opx_output_I.apply_to_config(qua_config) + channel.opx_output_Q.apply_to_config(qua_config) + + assert qua_config["controllers"] == { + "con1": { + "analog_inputs": { + 1: {"gain_db": 0, "shareable": False}, + }, + "analog_outputs": { + 1: {"delay": 0, "shareable": False}, + 2: {"delay": 0, "shareable": False}, + }, + } + } + + assert qua_config["elements"] == { + "in_out_channel": { + "intermediate_frequency": 0.0, + "mixInputs": { + "I": ("con1", 1), + "Q": ("con1", 2), + }, + "operations": {}, + "outputs": {"out1": ("con1", 1)}, + "smearing": 0, + "time_of_flight": 24, + } + } diff --git a/tests/components/channels/test_single_channel.py b/tests/components/channels/test_single_channel.py index f3caccf7..12564339 100644 --- a/tests/components/channels/test_single_channel.py +++ b/tests/components/channels/test_single_channel.py @@ -3,6 +3,7 @@ from copy import deepcopy from quam.components import * +from quam.components.ports.analog_outputs import OPXPlusAnalogOutputPort from quam.core import quam_dataclass, QuamRoot @@ -147,3 +148,46 @@ def test_instantiate_single_channel(): d_loaded = json.loads(d_json) instantiate_quam_class(SingleChannel, d_loaded) + + +def test_generate_config(qua_config): + channel = SingleChannel( + id="out_channel", + opx_output=("con1", 1), + ) + + channel.apply_to_config(qua_config) + + assert qua_config["controllers"] == { + "con1": { + "analog_outputs": { + 1: {"delay": 0, "shareable": False}, + }, + } + } + + assert qua_config["elements"] == { + "out_channel": {"operations": {}, "singleInput": {"port": ("con1", 1)}} + } + + +def test_generate_config_port(qua_config): + channel = SingleChannel( + id="out_channel", + opx_output=OPXPlusAnalogOutputPort("con1", 1), + ) + + channel.apply_to_config(qua_config) + channel.opx_output.apply_to_config(qua_config) + + assert qua_config["controllers"] == { + "con1": { + "analog_outputs": { + 1: {"delay": 0, "shareable": False}, + }, + } + } + + assert qua_config["elements"] == { + "out_channel": {"operations": {}, "singleInput": {"port": ("con1", 1)}} + } From 228323823e3c6bb445eefecd21a501f01ecd4d00 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 2 Jul 2024 19:59:18 +0200 Subject: [PATCH 24/31] adding ports containers --- quam/components/ports/__init__.py | 15 + quam/components/ports/ports_containers.py | 329 ++++++++++++++++++++++ 2 files changed, 344 insertions(+) create mode 100644 quam/components/ports/ports_containers.py diff --git a/quam/components/ports/__init__.py b/quam/components/ports/__init__.py index e69de29b..1aa4d70c 100644 --- a/quam/components/ports/__init__.py +++ b/quam/components/ports/__init__.py @@ -0,0 +1,15 @@ +from .analog_outputs import * +from .analog_inputs import * +from .base_ports import * +from .digital_inputs import * +from .digital_outputs import * +from .ports_containers import * + +__all__ = [ + *analog_outputs.__all__, + *analog_inputs.__all__, + *base_ports.__all__, + *digital_inputs.__all__, + *digital_outputs.__all__, + *ports_containers.__all__, +] diff --git a/quam/components/ports/ports_containers.py b/quam/components/ports/ports_containers.py new file mode 100644 index 00000000..1d43075e --- /dev/null +++ b/quam/components/ports/ports_containers.py @@ -0,0 +1,329 @@ +from abc import ABC, abstractmethod +from functools import partial, wraps +from typing import ClassVar, Dict, Union +from dataclasses import field +from quam.core import quam_dataclass, QuamComponent +from .analog_outputs import ( + OPXPlusAnalogOutputPort, + LFFEMAnalogOutputPort, + MWFEMAnalogOutputPort, +) +from .analog_inputs import ( + OPXPlusAnalogInputPort, + LFFEMAnalogInputPort, + MWFEMAnalogInputPort, +) +from .digital_outputs import OPXPlusDigitalOutputPort, FEMDigitalOutputPort +from .digital_inputs import OPXPlusDigitalInputPort + + +__all__ = ["OPXPlusPortsContainer", "FEMPortsContainer"] + + +# @quam_dataclass +# class PortsContainer(QuamComponent, ABC): + +# def _get_port( +# self, +# *ports_tuple, +# port_type: str, +# create: bool = False, +# **kwargs, +# ): + +# controllers = getattr(self, f"{port_type}s") + +# if not create: +# return controllers[controller_id][port_id] + +# ports = controllers.setdefault(controller_id, {}) + +# if port_type == "analog_output": +# ports[port_id] = OPXPlusAnalogOutputPort(controller_id, port_id, **kwargs) +# elif port_type == "analog_input": +# ports[port_id] = OPXPlusAnalogInputPort(controller_id, port_id, **kwargs) +# elif port_type == "digital_output": +# ports[port_id] = OPXPlusDigitalOutputPort(controller_id, port_id, **kwargs) +# elif port_type == "digital_input": +# ports[port_id] = OPXPlusDigitalInputPort(controller_id, port_id, **kwargs) +# else: +# raise ValueError(f"Invalid port type: {port_type}") + +# return ports[port_id] + +# def get_analog_output( +# self, +# controller_id: Union[str, int], +# port_id: int, +# create: bool = False, +# **kwargs, +# ): +# return self._get_port( +# controller_id, port_id, port_type="analog_output", create=create, **kwargs +# ) + +# def get_analog_input( +# self, +# controller_id: Union[str, int], +# port_id: int, +# create: bool = False, +# **kwargs, +# ): +# return self._get_port( +# controller_id, port_id, port_type="analog_input", create=create, **kwargs +# ) + +# def get_digital_output( +# self, +# controller_id: Union[str, int], +# port_id: int, +# create: bool = False, +# **kwargs, +# ): +# return self._get_port( +# controller_id, port_id, port_type="digital_output", create=create, **kwargs +# ) + +# def get_digital_input( +# self, +# controller_id: Union[str, int], +# port_id: int, +# create: bool = False, +# **kwargs, +# ): +# return self._get_port( +# controller_id, port_id, port_type="digital_input", create=create, **kwargs +# ) + + +@quam_dataclass +class OPXPlusPortsContainer(QuamComponent): + analog_outputs: Dict[Union[str, int], Dict[int, OPXPlusAnalogOutputPort]] = field( + default_factory=dict + ) + analog_inputs: Dict[Union[str, int], Dict[int, OPXPlusAnalogInputPort]] = field( + default_factory=dict + ) + digital_outputs: Dict[Union[str, int], Dict[int, OPXPlusDigitalOutputPort]] = field( + default_factory=dict + ) + digital_inputs: Dict[Union[str, int], Dict[int, OPXPlusDigitalInputPort]] = field( + default_factory=dict + ) + + def _get_port( + self, + controller_id: Union[str, int], + port_id: int, + port_type: str, + create: bool = False, + **kwargs, + ): + controllers = getattr(self, f"{port_type}s") + + if not create: + return controllers[controller_id][port_id] + + ports = controllers.setdefault(controller_id, {}) + + if port_type == "analog_output": + ports[port_id] = OPXPlusAnalogOutputPort(controller_id, port_id, **kwargs) + elif port_type == "analog_input": + ports[port_id] = OPXPlusAnalogInputPort(controller_id, port_id, **kwargs) + elif port_type == "digital_output": + ports[port_id] = OPXPlusDigitalOutputPort(controller_id, port_id, **kwargs) + elif port_type == "digital_input": + ports[port_id] = OPXPlusDigitalInputPort(controller_id, port_id, **kwargs) + else: + raise ValueError(f"Invalid port type: {port_type}") + + return ports[port_id] + + def get_analog_output( + self, + controller_id: Union[str, int], + port_id: int, + create: bool = False, + **kwargs, + ): + return self._get_port( + controller_id, port_id, port_type="analog_output", create=create, **kwargs + ) + + def get_analog_input( + self, + controller_id: Union[str, int], + port_id: int, + create: bool = False, + **kwargs, + ): + return self._get_port( + controller_id, port_id, port_type="analog_input", create=create, **kwargs + ) + + def get_digital_output( + self, + controller_id: Union[str, int], + port_id: int, + create: bool = False, + **kwargs, + ): + return self._get_port( + controller_id, port_id, port_type="digital_output", create=create, **kwargs + ) + + def get_digital_input( + self, + controller_id: Union[str, int], + port_id: int, + create: bool = False, + **kwargs, + ): + return self._get_port( + controller_id, port_id, port_type="digital_input", create=create, **kwargs + ) + + +@quam_dataclass +class FEMPortsContainer(QuamComponent): + num_port_elems: ClassVar[int] = 3 + analog_outputs: Dict[ + Union[str, int], Dict[int, Dict[int, LFFEMAnalogOutputPort]] + ] = field(default_factory=dict) + analog_inputs: Dict[Union[str, int], Dict[int, Dict[int, LFFEMAnalogInputPort]]] = ( + field(default_factory=dict) + ) + mw_outputs: Dict[Union[str, int], Dict[int, Dict[int, MWFEMAnalogOutputPort]]] = ( + field(default_factory=dict) + ) + mw_inputs: Dict[Union[str, int], Dict[int, Dict[int, MWFEMAnalogInputPort]]] = ( + field(default_factory=dict) + ) + digital_outputs: Dict[ + Union[str, int], Dict[int, Dict[int, FEMDigitalOutputPort]] + ] = field(default_factory=dict) + + def _get_port( + self, + controller_id: Union[str, int], + fem_id: int, + port_id: int, + port_type: str, + create: bool = False, + **kwargs, + ): + controllers = getattr(self, f"{port_type}s") + + if not create: + return controllers[controller_id][fem_id][port_id] + + fems = controllers.setdefault(controller_id, {}) + ports = fems.setdefault(controller_id, {}) + + if port_type == "analog_output": + ports[port_id] = LFFEMAnalogOutputPort(controller_id, fem_id, port_id) + elif port_type == "analog_input": + ports[port_id] = LFFEMAnalogInputPort(controller_id, fem_id, port_id) + elif port_type == "mw_output": + ports[port_id] = MWFEMAnalogOutputPort( + controller_id, fem_id, port_id, band=kwargs.get("band", 1) + ) + elif port_type == "mw_input": + ports[port_id] = MWFEMAnalogInputPort( + controller_id, + fem_id, + port_id, + band=kwargs.get("band", 1), # TODO Are default values the best here? + downconverter_frequency=kwargs.get("downconverter_frequency", 5e9), + ) + elif port_type == "digital_output": + ports[port_id] = FEMDigitalOutputPort(controller_id, fem_id, port_id) + else: + raise ValueError(f"Invalid port type: {port_type}") + + return ports[port_id] + + def get_analog_output( + self, + controller_id: Union[str, int], + fem_id: int, + port_id: int, + create: bool = False, + **kwargs, + ): + return self._get_port( + controller_id, + fem_id, + port_id, + port_type="analog_output", + create=create, + **kwargs, + ) + + def get_analog_input( + self, + controller_id: Union[str, int], + fem_id: int, + port_id: int, + create: bool = False, + **kwargs, + ): + return self._get_port( + controller_id, + fem_id, + port_id, + port_type="analog_input", + create=create, + **kwargs, + ) + + def get_mw_output( + self, + controller_id: Union[str, int], + fem_id: int, + port_id: int, + create: bool = False, + **kwargs, + ): + return self._get_port( + controller_id, + fem_id, + port_id, + port_type="mw_output", + create=create, + **kwargs, + ) + + def get_mw_input( + self, + controller_id: Union[str, int], + fem_id: int, + port_id: int, + create: bool = False, + **kwargs, + ): + return self._get_port( + controller_id, + fem_id, + port_id, + port_type="mw_input", + create=create, + **kwargs, + ) + + def get_digital_output( + self, + controller_id: Union[str, int], + fem_id: int, + port_id: int, + create: bool = False, + **kwargs, + ): + return self._get_port( + controller_id, + fem_id, + port_id, + port_type="digital_output", + create=create, + **kwargs, + ) From dfa64b03f6f17d539a6c19fa6e28de5ed203fe2f Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Tue, 2 Jul 2024 20:28:52 +0200 Subject: [PATCH 25/31] clean up ports containers --- quam/components/ports/ports_containers.py | 103 ++++------------------ 1 file changed, 15 insertions(+), 88 deletions(-) diff --git a/quam/components/ports/ports_containers.py b/quam/components/ports/ports_containers.py index 1d43075e..ff260c25 100644 --- a/quam/components/ports/ports_containers.py +++ b/quam/components/ports/ports_containers.py @@ -1,5 +1,3 @@ -from abc import ABC, abstractmethod -from functools import partial, wraps from typing import ClassVar, Dict, Union from dataclasses import field from quam.core import quam_dataclass, QuamComponent @@ -20,82 +18,6 @@ __all__ = ["OPXPlusPortsContainer", "FEMPortsContainer"] -# @quam_dataclass -# class PortsContainer(QuamComponent, ABC): - -# def _get_port( -# self, -# *ports_tuple, -# port_type: str, -# create: bool = False, -# **kwargs, -# ): - -# controllers = getattr(self, f"{port_type}s") - -# if not create: -# return controllers[controller_id][port_id] - -# ports = controllers.setdefault(controller_id, {}) - -# if port_type == "analog_output": -# ports[port_id] = OPXPlusAnalogOutputPort(controller_id, port_id, **kwargs) -# elif port_type == "analog_input": -# ports[port_id] = OPXPlusAnalogInputPort(controller_id, port_id, **kwargs) -# elif port_type == "digital_output": -# ports[port_id] = OPXPlusDigitalOutputPort(controller_id, port_id, **kwargs) -# elif port_type == "digital_input": -# ports[port_id] = OPXPlusDigitalInputPort(controller_id, port_id, **kwargs) -# else: -# raise ValueError(f"Invalid port type: {port_type}") - -# return ports[port_id] - -# def get_analog_output( -# self, -# controller_id: Union[str, int], -# port_id: int, -# create: bool = False, -# **kwargs, -# ): -# return self._get_port( -# controller_id, port_id, port_type="analog_output", create=create, **kwargs -# ) - -# def get_analog_input( -# self, -# controller_id: Union[str, int], -# port_id: int, -# create: bool = False, -# **kwargs, -# ): -# return self._get_port( -# controller_id, port_id, port_type="analog_input", create=create, **kwargs -# ) - -# def get_digital_output( -# self, -# controller_id: Union[str, int], -# port_id: int, -# create: bool = False, -# **kwargs, -# ): -# return self._get_port( -# controller_id, port_id, port_type="digital_output", create=create, **kwargs -# ) - -# def get_digital_input( -# self, -# controller_id: Union[str, int], -# port_id: int, -# create: bool = False, -# **kwargs, -# ): -# return self._get_port( -# controller_id, port_id, port_type="digital_input", create=create, **kwargs -# ) - - @quam_dataclass class OPXPlusPortsContainer(QuamComponent): analog_outputs: Dict[Union[str, int], Dict[int, OPXPlusAnalogOutputPort]] = field( @@ -121,8 +43,13 @@ def _get_port( ): controllers = getattr(self, f"{port_type}s") - if not create: + try: return controllers[controller_id][port_id] + except KeyError: + if not create: + raise KeyError( + f"Port {port_id} not found in controller {controller_id}" + ) ports = controllers.setdefault(controller_id, {}) @@ -145,7 +72,7 @@ def get_analog_output( port_id: int, create: bool = False, **kwargs, - ): + ) -> OPXPlusAnalogOutputPort: return self._get_port( controller_id, port_id, port_type="analog_output", create=create, **kwargs ) @@ -156,7 +83,7 @@ def get_analog_input( port_id: int, create: bool = False, **kwargs, - ): + ) -> OPXPlusAnalogInputPort: return self._get_port( controller_id, port_id, port_type="analog_input", create=create, **kwargs ) @@ -167,7 +94,7 @@ def get_digital_output( port_id: int, create: bool = False, **kwargs, - ): + ) -> OPXPlusDigitalOutputPort: return self._get_port( controller_id, port_id, port_type="digital_output", create=create, **kwargs ) @@ -178,7 +105,7 @@ def get_digital_input( port_id: int, create: bool = False, **kwargs, - ): + ) -> OPXPlusDigitalInputPort: return self._get_port( controller_id, port_id, port_type="digital_input", create=create, **kwargs ) @@ -250,7 +177,7 @@ def get_analog_output( port_id: int, create: bool = False, **kwargs, - ): + ) -> LFFEMAnalogOutputPort: return self._get_port( controller_id, fem_id, @@ -267,7 +194,7 @@ def get_analog_input( port_id: int, create: bool = False, **kwargs, - ): + ) -> LFFEMAnalogInputPort: return self._get_port( controller_id, fem_id, @@ -284,7 +211,7 @@ def get_mw_output( port_id: int, create: bool = False, **kwargs, - ): + ) -> MWFEMAnalogOutputPort: return self._get_port( controller_id, fem_id, @@ -301,7 +228,7 @@ def get_mw_input( port_id: int, create: bool = False, **kwargs, - ): + ) -> MWFEMAnalogInputPort: return self._get_port( controller_id, fem_id, @@ -318,7 +245,7 @@ def get_digital_output( port_id: int, create: bool = False, **kwargs, - ): + ) -> FEMDigitalOutputPort: return self._get_port( controller_id, fem_id, From 742385b63ae375803930a236c5e86cb5f2111a70 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 3 Jul 2024 07:56:53 +0200 Subject: [PATCH 26/31] add porots containers tests --- quam/components/ports/ports_containers.py | 89 +++++++++++++-- .../components/ports/test_ports_containers.py | 106 ++++++++++++++++++ 2 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 tests/components/ports/test_ports_containers.py diff --git a/quam/components/ports/ports_containers.py b/quam/components/ports/ports_containers.py index ff260c25..4805305a 100644 --- a/quam/components/ports/ports_containers.py +++ b/quam/components/ports/ports_containers.py @@ -1,5 +1,6 @@ -from typing import ClassVar, Dict, Union +from typing import ClassVar, Dict, Optional, TypeVar, Union from dataclasses import field +from quam.components.ports.base_ports import FEMPort from quam.core import quam_dataclass, QuamComponent from .analog_outputs import ( OPXPlusAnalogOutputPort, @@ -17,6 +18,20 @@ __all__ = ["OPXPlusPortsContainer", "FEMPortsContainer"] +OPXPlusPortTypes = Union[ + OPXPlusAnalogInputPort, + OPXPlusAnalogOutputPort, + OPXPlusDigitalOutputPort, + OPXPlusDigitalInputPort, +] +FEMPortTypes = Union[ + LFFEMAnalogInputPort, + LFFEMAnalogOutputPort, + MWFEMAnalogInputPort, + MWFEMAnalogOutputPort, + FEMDigitalOutputPort, +] + @quam_dataclass class OPXPlusPortsContainer(QuamComponent): @@ -48,10 +63,12 @@ def _get_port( except KeyError: if not create: raise KeyError( - f"Port {port_id} not found in controller {controller_id}" + f"Could not find existing {port_type} port: " + f"{port_type} ({controller_id}, {port_id}" ) - ports = controllers.setdefault(controller_id, {}) + controllers.setdefault(controller_id, {}) + ports = controllers[controller_id] if port_type == "analog_output": ports[port_id] = OPXPlusAnalogOutputPort(controller_id, port_id, **kwargs) @@ -66,6 +83,19 @@ def _get_port( return ports[port_id] + def get_port_from_reference( + self, port_reference: str, create=False + ) -> OPXPlusPortTypes: + elems = port_reference.split("/") + port_type, controller_id, port_id = elems[-3:] + + if controller_id.isdigit(): + controller_id = int(controller_id) + fem_id = int(fem_id) + port_id = int(port_id) + + return self._get_port(controller_id, fem_id, port_id, port_type, create=create) + def get_analog_output( self, controller_id: Union[str, int], @@ -141,19 +171,33 @@ def _get_port( ): controllers = getattr(self, f"{port_type}s") - if not create: + try: return controllers[controller_id][fem_id][port_id] + except KeyError: + if not create: + raise KeyError( + f"Could not find existing {port_type} port: " + f"{port_type} ({controller_id}, {fem_id}, {port_id}" + ) - fems = controllers.setdefault(controller_id, {}) - ports = fems.setdefault(controller_id, {}) + controllers.setdefault(controller_id, {}) + fems = controllers[controller_id] + fems.setdefault(fem_id, {}) + ports = fems[fem_id] if port_type == "analog_output": - ports[port_id] = LFFEMAnalogOutputPort(controller_id, fem_id, port_id) + ports[port_id] = LFFEMAnalogOutputPort( + controller_id, fem_id, port_id, **kwargs + ) elif port_type == "analog_input": - ports[port_id] = LFFEMAnalogInputPort(controller_id, fem_id, port_id) + ports[port_id] = LFFEMAnalogInputPort( + controller_id, fem_id, port_id, **kwargs + ) elif port_type == "mw_output": + if "upconverter_frequency" not in kwargs and "upconverters" not in kwargs: + kwargs["upconverter_frequency"] = 5e9 ports[port_id] = MWFEMAnalogOutputPort( - controller_id, fem_id, port_id, band=kwargs.get("band", 1) + controller_id, fem_id, port_id, band=kwargs.get("band", 1), **kwargs ) elif port_type == "mw_input": ports[port_id] = MWFEMAnalogInputPort( @@ -162,14 +206,39 @@ def _get_port( port_id, band=kwargs.get("band", 1), # TODO Are default values the best here? downconverter_frequency=kwargs.get("downconverter_frequency", 5e9), + **kwargs, ) elif port_type == "digital_output": - ports[port_id] = FEMDigitalOutputPort(controller_id, fem_id, port_id) + ports[port_id] = FEMDigitalOutputPort( + controller_id, fem_id, port_id, **kwargs + ) else: raise ValueError(f"Invalid port type: {port_type}") return ports[port_id] + def get_port_from_reference( + self, + port_reference: Union[QuamComponent, str], + attr: Optional[str] = None, + create=False, + ) -> FEMPortTypes: + if isinstance(port_reference, QuamComponent): + reference = port_reference.get_reference(attr=attr) + if reference is None: + raise ValueError("Cannot get port from reference {port_reference}") + port_reference = reference + + elems = port_reference.split("/") + port_type, controller_id, fem_id, port_id = elems[-4:] + + if controller_id.isdigit(): + controller_id = int(controller_id) + fem_id = int(fem_id) + port_id = int(port_id) + + return self._get_port(controller_id, fem_id, port_id, port_type, create=create) + def get_analog_output( self, controller_id: Union[str, int], diff --git a/tests/components/ports/test_ports_containers.py b/tests/components/ports/test_ports_containers.py new file mode 100644 index 00000000..bfbeb5d0 --- /dev/null +++ b/tests/components/ports/test_ports_containers.py @@ -0,0 +1,106 @@ +import pytest +from quam.components.ports import ( + OPXPlusPortsContainer, + FEMPortsContainer, + OPXPlusAnalogOutputPort, + LFFEMAnalogOutputPort, +) +from quam.components.ports.analog_inputs import ( + LFFEMAnalogInputPort, + MWFEMAnalogInputPort, + OPXPlusAnalogInputPort, +) +from quam.components.ports.analog_outputs import MWFEMAnalogOutputPort +from quam.components.ports.digital_inputs import OPXPlusDigitalInputPort +from quam.components.ports.digital_outputs import ( + FEMDigitalOutputPort, + OPXPlusDigitalOutputPort, +) + + +def test_opx_plus_ports_container_initialize(): + ports_container = OPXPlusPortsContainer() + assert ports_container.analog_outputs == {} + assert ports_container.analog_inputs == {} + assert ports_container.digital_outputs == {} + assert ports_container.digital_inputs == {} + + +def test_fem_ports_container_initialize(): + ports_container = FEMPortsContainer() + assert ports_container.analog_outputs == {} + assert ports_container.analog_inputs == {} + assert ports_container.mw_outputs == {} + assert ports_container.mw_inputs == {} + assert ports_container.digital_outputs == {} + + +@pytest.mark.parametrize( + "port_type", ["analog_output", "analog_input", "digital_output", "digital_input"] +) +def test_opx_plus_ports_container_add_ports(port_type): + port_mapping = { + "analog_output": OPXPlusAnalogOutputPort, + "analog_input": OPXPlusAnalogInputPort, + "digital_output": OPXPlusDigitalOutputPort, + "digital_input": OPXPlusDigitalInputPort, + } + + ports_container = OPXPlusPortsContainer() + + get_port_func = getattr(ports_container, f"get_{port_type}") + + with pytest.raises(KeyError): + get_port_func("con1", 2) + + port = get_port_func("con2", 3, create=True) + assert isinstance(port, port_mapping[port_type]) + + assert port.controller_id == "con2" + assert port.port_id == 3 + + port2 = get_port_func("con2", 3, create=False) + assert port is port2 + + port3 = get_port_func("con2", 3, create=True) + assert port is port3 + + ports_group = getattr(ports_container, f"{port_type}s") + assert port is ports_group["con2"][3] + + +@pytest.mark.parametrize( + "port_type", + ["analog_output", "analog_input", "mw_output", "mw_input", "digital_output"], +) +def test_fem_ports_container_add_ports(port_type): + port_mapping = { + "analog_output": LFFEMAnalogOutputPort, + "analog_input": LFFEMAnalogInputPort, + "mw_output": MWFEMAnalogOutputPort, + "mw_input": MWFEMAnalogInputPort, + "digital_output": FEMDigitalOutputPort, + } + + ports_container = FEMPortsContainer() + + get_port_func = getattr(ports_container, f"get_{port_type}") + + with pytest.raises(KeyError): + get_port_func("con1", 1, 2) + + port = get_port_func("con2", 2, 3, create=True) + assert isinstance(port, port_mapping[port_type]) + + assert port.controller_id == "con2" + assert port.fem_id == 2 + assert port.port_id == 3 + + port2 = get_port_func("con2", 2, 3, create=False) + assert port is port2 + + port3 = get_port_func("con2", 2, 3, create=True) + assert port is port3 + + ports_group = getattr(ports_container, f"{port_type}s") + assert port is ports_group["con2"][2][3] From 514e4e062b16c678eda6b7b933674a4e36b7cb65 Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Wed, 3 Jul 2024 08:05:51 +0200 Subject: [PATCH 27/31] test ports containers --- quam/components/ports/ports_containers.py | 11 ++- .../components/ports/test_ports_containers.py | 71 +++++++++++++++++++ 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/quam/components/ports/ports_containers.py b/quam/components/ports/ports_containers.py index 4805305a..bea29d54 100644 --- a/quam/components/ports/ports_containers.py +++ b/quam/components/ports/ports_containers.py @@ -83,18 +83,16 @@ def _get_port( return ports[port_id] - def get_port_from_reference( - self, port_reference: str, create=False - ) -> OPXPlusPortTypes: + def reference_to_port(self, port_reference: str, create=False) -> OPXPlusPortTypes: elems = port_reference.split("/") port_type, controller_id, port_id = elems[-3:] + port_type = port_type[:-1] if controller_id.isdigit(): controller_id = int(controller_id) - fem_id = int(fem_id) port_id = int(port_id) - return self._get_port(controller_id, fem_id, port_id, port_type, create=create) + return self._get_port(controller_id, port_id, port_type, create=create) def get_analog_output( self, @@ -217,7 +215,7 @@ def _get_port( return ports[port_id] - def get_port_from_reference( + def reference_to_port( self, port_reference: Union[QuamComponent, str], attr: Optional[str] = None, @@ -232,6 +230,7 @@ def get_port_from_reference( elems = port_reference.split("/") port_type, controller_id, fem_id, port_id = elems[-4:] + port_type = port_type[:-1] if controller_id.isdigit(): controller_id = int(controller_id) fem_id = int(fem_id) diff --git a/tests/components/ports/test_ports_containers.py b/tests/components/ports/test_ports_containers.py index bfbeb5d0..774e1ec0 100644 --- a/tests/components/ports/test_ports_containers.py +++ b/tests/components/ports/test_ports_containers.py @@ -69,6 +69,40 @@ def test_opx_plus_ports_container_add_ports(port_type): assert port is ports_group["con2"][3] +@pytest.mark.parametrize( + "port_type", ["analog_output", "analog_input", "digital_output", "digital_input"] +) +def test_opx_plus_ports_container_reference_to_port(port_type): + port_mapping = { + "analog_output": OPXPlusAnalogOutputPort, + "analog_input": OPXPlusAnalogInputPort, + "digital_output": OPXPlusDigitalOutputPort, + "digital_input": OPXPlusDigitalInputPort, + } + + ports_container = OPXPlusPortsContainer() + + port_reference = f"#/{port_type}s/con1/3" + + with pytest.raises(KeyError): + ports_container.reference_to_port(port_reference) + + port = ports_container.reference_to_port(port_reference, create=True) + assert isinstance(port, port_mapping[port_type]) + + assert port.controller_id == "con1" + assert port.port_id == 3 + + port2 = ports_container.reference_to_port(port_reference, create=False) + assert port is port2 + + port3 = ports_container.reference_to_port(port_reference, create=True) + assert port is port3 + + ports_group = getattr(ports_container, f"{port_type}s") + assert port is ports_group["con1"][3] + + @pytest.mark.parametrize( "port_type", ["analog_output", "analog_input", "mw_output", "mw_input", "digital_output"], @@ -104,3 +138,40 @@ def test_fem_ports_container_add_ports(port_type): ports_group = getattr(ports_container, f"{port_type}s") assert port is ports_group["con2"][2][3] + + +@pytest.mark.parametrize( + "port_type", + ["analog_output", "analog_input", "mw_output", "mw_input", "digital_output"], +) +def test_fem_ports_container_reference_to_port(port_type): + port_mapping = { + "analog_output": LFFEMAnalogOutputPort, + "analog_input": LFFEMAnalogInputPort, + "mw_output": MWFEMAnalogOutputPort, + "mw_input": MWFEMAnalogInputPort, + "digital_output": FEMDigitalOutputPort, + } + + port_reference = f"#/{port_type}s/con1/2/3" + + ports_container = FEMPortsContainer() + + with pytest.raises(KeyError): + ports_container.reference_to_port(port_reference) + + port = ports_container.reference_to_port(port_reference, create=True) + assert isinstance(port, port_mapping[port_type]) + + assert port.controller_id == "con1" + assert port.fem_id == 2 + assert port.port_id == 3 + + port2 = ports_container.reference_to_port(port_reference, create=False) + assert port is port2 + + port3 = ports_container.reference_to_port(port_reference, create=True) + assert port is port3 + + ports_group = getattr(ports_container, f"{port_type}s") + assert port is ports_group["con1"][2][3] From bae1b09883de8207cb34fc5d93dbbb80fa13744f Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 4 Jul 2024 04:45:51 +0800 Subject: [PATCH 28/31] fixing ports bugs --- quam/components/ports/ports_containers.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/quam/components/ports/ports_containers.py b/quam/components/ports/ports_containers.py index bea29d54..1e4509b5 100644 --- a/quam/components/ports/ports_containers.py +++ b/quam/components/ports/ports_containers.py @@ -2,6 +2,7 @@ from dataclasses import field from quam.components.ports.base_ports import FEMPort from quam.core import quam_dataclass, QuamComponent +from quam.core.quam_classes import QuamBase from .analog_outputs import ( OPXPlusAnalogOutputPort, LFFEMAnalogOutputPort, @@ -83,7 +84,17 @@ def _get_port( return ports[port_id] - def reference_to_port(self, port_reference: str, create=False) -> OPXPlusPortTypes: + def reference_to_port( + self, + port_reference: Union[QuamComponent, str], + attr: Optional[str] = None, + create=False, + ) -> OPXPlusPortTypes: + if isinstance(port_reference, QuamComponent): + reference = port_reference.get_reference(attr=attr) + if reference is None: + raise ValueError("Cannot get port from reference {port_reference}") + port_reference = reference elems = port_reference.split("/") port_type, controller_id, port_id = elems[-3:] @@ -221,7 +232,7 @@ def reference_to_port( attr: Optional[str] = None, create=False, ) -> FEMPortTypes: - if isinstance(port_reference, QuamComponent): + if isinstance(port_reference, QuamBase): reference = port_reference.get_reference(attr=attr) if reference is None: raise ValueError("Cannot get port from reference {port_reference}") From 50010155a9c8f042efee7e019e45a2784529b1bd Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 4 Jul 2024 04:46:30 +0800 Subject: [PATCH 29/31] octave ports --- quam/components/octave.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/quam/components/octave.py b/quam/components/octave.py index a3e36ec7..ff2b1a56 100644 --- a/quam/components/octave.py +++ b/quam/components/octave.py @@ -3,6 +3,8 @@ from typing import Any, Optional, Union, ClassVar, Dict, List, Tuple, Literal from dataclasses import field +from quam.components.ports.analog_outputs import LFAnalogOutputPort +from quam.components.ports.base_ports import BasePort from quam.core import QuamComponent, quam_dataclass from quam.components.hardware import BaseFrequencyConverter, FrequencyConverter from quam.components.channels import ( @@ -274,10 +276,19 @@ def apply_to_config(self, config: Dict) -> None: "input_attenuators": self.input_attenuators, } if isinstance(self.channel, SingleChannel): - output_config["I_connection"] = self.channel.opx_output + if isinstance(self.channel.opx_output, LFAnalogOutputPort): + output_config["I_connection"] = self.channel.opx_output.port_tuple + else: + output_config["I_connection"] = self.channel.opx_output elif isinstance(self.channel, IQChannel): - output_config["I_connection"] = tuple(self.channel.opx_output_I) - output_config["Q_connection"] = tuple(self.channel.opx_output_Q) + if isinstance(self.channel.opx_output_I, LFAnalogOutputPort): + output_config["I_connection"] = self.channel.opx_output_I.port_tuple + else: + output_config["I_connection"] = tuple(self.channel.opx_output_I) + if isinstance(self.channel.opx_output_Q, LFAnalogOutputPort): + output_config["Q_connection"] = self.channel.opx_output_Q.port_tuple + else: + output_config["Q_connection"] = tuple(self.channel.opx_output_Q) @quam_dataclass @@ -379,14 +390,20 @@ def apply_to_config(self, config: Dict) -> None: IF_channels = [] opx_channels = [] + opx_port_tuples = [ + p.port_tuple if isinstance(p, BasePort) else tuple(p) for p in opx_channels + ] + IF_config = config["octaves"][self.octave.name]["IF_outputs"] - for k, (IF_ch, opx_ch) in enumerate(zip(IF_channels, opx_channels), start=1): + for k, (IF_ch, opx_port_tuples) in enumerate( + zip(IF_channels, opx_port_tuples), start=1 + ): label = f"IF_out{IF_ch}" - IF_config.setdefault(label, {"port": tuple(opx_ch), "name": f"out{k}"}) - if IF_config[label]["port"] != tuple(opx_ch): + IF_config.setdefault(label, {"port": opx_port_tuples, "name": f"out{k}"}) + if IF_config[label]["port"] != opx_port_tuples: raise ValueError( f"Error generating config for Octave downconverter id={self.id}: " - f"Unable to assign {label} to port {opx_ch} because it is already " + f"Unable to assign {label} to port {opx_port_tuples} because it is already " f"assigned to port {IF_config[label]['port']} " ) From e044810a4d80e0f6937f90653446a33080c555fb Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 4 Jul 2024 11:01:52 +0800 Subject: [PATCH 30/31] test: fix IQ channels test --- tests/components/channels/test_IQ_channel.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/components/channels/test_IQ_channel.py b/tests/components/channels/test_IQ_channel.py index fd1892bd..90eaeb4c 100644 --- a/tests/components/channels/test_IQ_channel.py +++ b/tests/components/channels/test_IQ_channel.py @@ -100,8 +100,6 @@ def test_generate_config(qua_config): ) channel.apply_to_config(qua_config) - channel.opx_output_I.apply_to_config(qua_config) - channel.opx_output_Q.apply_to_config(qua_config) assert qua_config["controllers"] == { "con1": { From f28562d7c2c43f278aab1ed71496602af78ea98c Mon Sep 17 00:00:00 2001 From: Serwan Asaad Date: Thu, 4 Jul 2024 12:37:44 +0800 Subject: [PATCH 31/31] fix: dataclass issue for python > 3.9 --- quam/components/ports/base_ports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quam/components/ports/base_ports.py b/quam/components/ports/base_ports.py index f9756d9e..d1c82ab7 100644 --- a/quam/components/ports/base_ports.py +++ b/quam/components/ports/base_ports.py @@ -49,7 +49,7 @@ def apply_to_config(self, config: Dict) -> None: self._update_port_config(port_cfg, port_properties) -@quam_dataclass +@quam_dataclass(kw_only=False) class OPXPlusPort(BasePort, ABC): controller_id: Union[str, int] port_id: int @@ -78,7 +78,7 @@ def get_port_config( return port_cfg -@quam_dataclass +@quam_dataclass(kw_only=False) class FEMPort(BasePort, ABC): fem_type: ClassVar[str] controller_id: Union[str, int]