Skip to content

Custom QUAM Components

To create custom QUAM components, their classes should be defined in a Python module that can be accessed from Python. The reason for this is that otherwise QUAM cannot load QUAM from a JSON file as it cannot determine where the classes are defined. If you already have a Python module that you use for your own QUA code, it is recommended to add QUAM components to that module. If you don't already have such a module, please follow the guide below.

Creating a Custom Python Module

Here we describe how to create a minimal Python module that can be used for your custom QUAM components. In this example, we will give the top-level folder the name my-quam and the Python module will be called my_quam (note the underscore instead of dash). First create the following folder structure

my-quam
├── my_quam
│   └── __init__.py
│   └── components
│       └── __init__.py
└── pyproject.toml
The __init__.py files should be empty, and pyproject.toml should have the following contents:

pyproject.toml
[project]
name = "my-quam"
version = "0.1.0"
description = "User QUAM repository"
authors = [{ name = "Jane Doe", email = "jane.doe@quantum-machines.co" }]
requires-python = ">=3.9"

[build-system]
requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"


[tool.setuptools]
packages = ["my_quam"]

Feel free to modify details such as description and authors.

Finally, to install the package, first make sure you're in the correct environment, then navigate to the top-level folder my-quam and run:

pip install .
The custom QUAM components can then be loaded as
from my_quam.components import *
All the custom QUAM components should be placed as Python files in my-quam/my_quam/components.

Creating a Custom QUAM Component

Once a designated Python module has been chosen / created, it can be populated with a custom component. We will assume that the newly-created Python module my_quam is used. In this example, we will make a basic QUAM component representing a DC gate, with two properties: name and dc_voltage:

my_quam/components/gates.py
from typing import Union
from quam.core import QuamComponent, quam_dataclass

@quam_dataclass
class DcGate(QuamComponent):
    id: Union[int, str]
    dc_voltage: float
which can be instantiated as follows:
from my_quam.components.gates import DcGate
dc_gate = DcGate(id="plunger_gate", dc_voltage=0.43)

A few notes about the above:

  • Each QuamComponent inherits from QuamComponent.
  • QUAM components are decorated with @quam_dataclass, which is a variant of the Python @dataclass.
Reason for @quam_dataclass instead of @dataclass

Inheriting from a dataclass is not directly possible when the parent class has keyword arguments and the child class does not. To illustrate this, the following example will raise a TypeError:

@dataclass
class Parent:
    optional_attr: int = 42

@dataclass
class Child(Parent):
    required_attr: int

In Python 3.10 and up, this can be solved by adding the kw_only=True keyword argument:

@dataclass
class Parent:
    optional_attr: int = 42

@dataclass(kw_only=True)
class Child(Parent):
    required_attr: int

child = Child(required_attr=12)  # Note that we now need to explicitly pass keywords

The keyword kw_only was only introduced in Python 3.10, and so the example above would raise an error in Python <3.10. However, to ensure QUAM is compatible with Python 3.9 and above, we introduced @quam_dataclass which fixes this problem:

@quam_dataclass
class Child(Parent):
    required_attr: int

An additional benefit is that kw_only=True is automatically passed along.
From Python 3.10 onwards, @quam_dataclass is equivalent to @dataclass(kw_only=True, eq=False)

QUAM Component Subclassing

QUAM components can also be subclassed to add functionalities to the parent class. For example, we now want to combine a DC and AC gate together, where the AC part corresponds to an OPX channel. To do this, we create a class called AcDcGate that inherits from both DcGate and SingleChannel:

from quam.components import SingleChannel


@quam_dataclass
class AcDcGate(DcGate, SingleChannel):
    pass

It can be instantiated using

ac_dc_gate = AcDcGate(id="plunger_gate", dc_voltage=0.43, opx_output=("con1", 1))

Notice that the keyword argument opx_output now also needs to be passed. This is because it's a required argument for SingleChannel.

Cross-Component Dependencies

Components in separate files sometimes need to reference each other's types — for example, a Qubit that holds a Resonator and a Resonator that back-references its parent Qubit. Importing each class directly in the other's module creates a circular import error.

The standard solution is to guard the import with TYPE_CHECKING and add from __future__ import annotations so the annotation is treated as a string at runtime rather than resolved immediately:

my_quam/components/qubit.py
from __future__ import annotations
from typing import TYPE_CHECKING
from quam.core import QuamComponent, quam_dataclass

if TYPE_CHECKING:
    from my_quam.components.resonator import Resonator

@quam_dataclass
class Qubit(QuamComponent):
    resonator: Resonator
my_quam/components/resonator.py
from __future__ import annotations
from typing import TYPE_CHECKING
from quam.core import QuamComponent, quam_dataclass

if TYPE_CHECKING:
    from my_quam.components.qubit import Qubit

@quam_dataclass
class Resonator(QuamComponent):
    qubit: Qubit

QUAM saves and loads these components without any extra configuration. During loading, the __class__ key present in every serialized QuAM dict identifies the concrete type, so the deferred annotation is never needed at runtime.