Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Added helper function `_create_port_property_deprecation_message()` to provide detailed migration guidance in deprecation warnings
- Added `quam.serialization.include_defaults` config field to control whether default values are included in serialized JSON (defaults to `True`)
- Added v2→v3 config migration with automatic upgrade support for the new serialization settings
- Added support for channels as quantum components via multiple inheritance, enabling channel-level macros and operations (e.g., `class HybridChannel(SingleChannel, Qubit)`). This allows macros to be attached directly to channels instead of requiring a parent qubit component.
- Added `skip_save` field metadata support to exclude specific dataclass fields from serialization while keeping them accessible at runtime. Use `field(metadata={"skip_save": True})` to mark fields that should not be saved to JSON

### Changed
Expand Down
34 changes: 34 additions & 0 deletions docs/components/qubits-and-qubit-pairs.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,40 @@ q1.align() # Synchronizes all channels of q1
q1.align(q2) # Synchronizes all channels of q1 and q2
```

### Channels as Quantum Components

Channels can also be quantum components via multiple inheritance, enabling macros and operations to be attached directly at the channel level. This is useful when the channel itself is the natural abstraction for an operation, such as flattop pulse macros in spectroscopy experiments.

```python
from quam.components.channels import SingleChannel
from quam.components.quantum_components import Qubit

# Create a hybrid channel-qubit class
@quam_dataclass
class ChannelAsQubit(SingleChannel, Qubit):
"""Channel that also acts as a quantum component with macros"""
pass

# Use with channel operations and macros
ch = ChannelAsQubit(id="spectroscopy", opx_output=("con1", 1))

# Channel operations
ch.play("readout_pulse")

# Qubit operations (channels included in ch.channels)
ch.align()

# Macros attached directly to the channel
@ch.register_macro
def flattop(self):
"""Flattop pulse macro for this channel"""
pass

ch.apply("flattop")
```

When a channel inherits from `Qubit`, it automatically includes itself in its `channels` dictionary, enabling operations like `get_pulse()` and `align()` to work seamlessly with channel-level pulses and macros.

## Qubit Pairs

The `QubitPair` class models the interaction between two qubits, managing:
Expand Down
6 changes: 5 additions & 1 deletion quam/components/quantum_components/qubit.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,17 @@ def name(self) -> str:
@property
def channels(self) -> Dict[str, Channel]:
"""Returns a dictionary of all channels of the qubit"""
return {
channels = {
key: val
for key, val in self.get_attrs(
follow_references=True, include_defaults=True
).items()
if isinstance(val, Channel)
}
# If the qubit itself is a Channel, include it in the channels dictionary.
if isinstance(self, Channel):
channels[self.name] = self
return channels

def get_pulse(self, pulse_name: str) -> Pulse:
"""Returns the pulse with the given name
Expand Down
173 changes: 173 additions & 0 deletions tests/components/test_hybrid_channel_component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
from typing import Dict, Any
import pytest
from quam.components.channels import SingleChannel, Channel
from quam.components.quantum_components.qubit import Qubit
from quam.components.quantum_components.quantum_component import QuantumComponent
from quam.core import quam_dataclass, QuamRoot
from quam.components.ports import LFFEMAnalogOutputPort
from quam.components.pulses import Pulse

@quam_dataclass
class HybridChannelFirst(SingleChannel, Qubit):
"""Hybrid class where Channel comes first in MRO."""
pass


@quam_dataclass
class HybridQubitFirst(Qubit, SingleChannel):
"""Hybrid class where Qubit comes first in MRO."""
pass


def test_hybrid_channel_first_instantiation(qua_config):
"""Test instantiation of HybridChannelFirst (Channel logic dominates)."""
hybrid = HybridChannelFirst(
id="test_c1",
opx_output=LFFEMAnalogOutputPort("con1", 1, 1)
)

# Check MRO-based defaults/logic
# Channel.name implementation: if id is str, return id.
assert hybrid.name == "test_c1"
assert hybrid.id == "test_c1"

# Check config generation
hybrid.apply_to_config(qua_config)

assert "test_c1" in qua_config["elements"]
assert "singleInput" in qua_config["elements"]["test_c1"]

# Check that it is recognized as both
assert isinstance(hybrid, Channel)
assert isinstance(hybrid, QuantumComponent)
assert isinstance(hybrid, Qubit)


def test_hybrid_qubit_first_instantiation(qua_config):
"""Test instantiation of HybridQubitFirst (Qubit logic dominates)."""
hybrid = HybridQubitFirst(
id="test_q1",
opx_output=LFFEMAnalogOutputPort("con1", 1, 2)
)

# Qubit.name implementation: if id is str, return id.
assert hybrid.name == "test_q1"

# Config generation should still work via SingleChannel.apply_to_config
hybrid.apply_to_config(qua_config)

assert "test_q1" in qua_config["elements"]
assert "singleInput" in qua_config["elements"]["test_q1"]


def test_hybrid_int_id_naming():
"""Test naming behavior with integer IDs."""

# Channel first: defaults to "ch{id}"
h1 = HybridChannelFirst(
id=1,
opx_output=LFFEMAnalogOutputPort("con1", 1, 1)
)
# Channel._default_label is "ch"
assert h1.name == "ch1"

# Qubit first: defaults to "q{id}"
h2 = HybridQubitFirst(
id=2,
opx_output=LFFEMAnalogOutputPort("con1", 1, 2)
)
# Qubit logic
assert h2.name == "q2"


def test_hybrid_with_root_config():
"""Test full config generation within a QuamRoot."""
@quam_dataclass
class MyRoot(QuamRoot):
component: HybridChannelFirst

root = MyRoot(
component=HybridChannelFirst(
id=1,
opx_output=LFFEMAnalogOutputPort("con1", 1, 1)
)
)

config = root.generate_config()
assert "ch1" in config["elements"]
assert config["elements"]["ch1"]["singleInput"]["port"] == ("con1", 1, 1)


def test_conflicting_defaults():
"""Verify handling of default values."""
# Qubit has id default "#./inferred_id"
# Channel has id default None

# If Qubit is first, id default should be "#./inferred_id"
h_q = HybridQubitFirst(opx_output=LFFEMAnalogOutputPort("con1", 1, 1))
assert h_q.id == "#./inferred_id"

# If Channel is first, id default should be None
h_c = HybridChannelFirst(opx_output=LFFEMAnalogOutputPort("con1", 1, 1))
assert h_c.id is None

def test_qubit_get_pulse_on_hybrid():
"""Test if Qubit.get_pulse works when the pulse is on the hybrid object itself."""
from quam.components.pulses import SquarePulse
pulse = SquarePulse(length=100, amplitude=0.5)
hybrid = HybridChannelFirst(
id="test_c1",
opx_output=LFFEMAnalogOutputPort("con1", 1, 1),
operations={"readout": pulse}
)

# Qubit.get_pulse iterates over self.channels.
# If self.channels doesn't include self, this will fail.
try:
found_pulse = hybrid.get_pulse("readout")
assert found_pulse is pulse
except ValueError as e:
pytest.fail(f"Qubit.get_pulse failed to find pulse on hybrid object: {e}")


def test_hybrid_channels_dict_key_matches_name():
"""Verify that when a Qubit is a Channel, channels dict uses self.name as key."""
from quam.components.pulses import SquarePulse

# Test with HybridChannelFirst and string ID
hybrid_cf = HybridChannelFirst(
id="test_channel",
opx_output=LFFEMAnalogOutputPort("con1", 1, 1),
operations={"readout": SquarePulse(length=100, amplitude=0.5)}
)
assert hybrid_cf.name == "test_channel"
assert hybrid_cf.name in hybrid_cf.channels
assert hybrid_cf.channels[hybrid_cf.name] is hybrid_cf

# Test with HybridQubitFirst and string ID
hybrid_qf = HybridQubitFirst(
id="test_qubit",
opx_output=LFFEMAnalogOutputPort("con1", 1, 2),
operations={"readout": SquarePulse(length=100, amplitude=0.5)}
)
assert hybrid_qf.name == "test_qubit"
assert hybrid_qf.name in hybrid_qf.channels
assert hybrid_qf.channels[hybrid_qf.name] is hybrid_qf

# Test with HybridChannelFirst and integer ID
hybrid_cf_int = HybridChannelFirst(
id=5,
opx_output=LFFEMAnalogOutputPort("con1", 1, 3),
)
assert hybrid_cf_int.name == "ch5"
assert hybrid_cf_int.name in hybrid_cf_int.channels
assert hybrid_cf_int.channels[hybrid_cf_int.name] is hybrid_cf_int

# Test with HybridQubitFirst and integer ID
hybrid_qf_int = HybridQubitFirst(
id=7,
opx_output=LFFEMAnalogOutputPort("con1", 1, 4),
)
assert hybrid_qf_int.name == "q7"
assert hybrid_qf_int.name in hybrid_qf_int.channels
assert hybrid_qf_int.channels[hybrid_qf_int.name] is hybrid_qf_int
Loading