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
26 changes: 26 additions & 0 deletions .github/workflows/quam_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ on:
branches: [ main ]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: pyproject.toml

- name: Install dev dependencies
run: |
python -m pip install --upgrade pip
pip install .[dev]

- name: Black formatting check
run: poe check-format

- name: Flake8 linting
run: poe lint

- name: Mypy type check
run: poe typecheck

run-tests:
uses: ./.github/workflows/run_tests.yaml
secrets: inherit
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
## [Unreleased]

### Added

- Added `mypy` type checking and a `poethepoet` task runner to the dev workflow: `poe format` / `poe check-format` (black), `poe lint` (flake8), `poe typecheck` (mypy), `poe test` (pytest), and a combined `poe check` that runs them all.

### Changed

- Annotated the codebase and resolved all `mypy` errors, alongside black formatting. These are internal type-correctness and formatting changes only — no public API or runtime behavior changes.

## [v0.6.0]

### Added
Expand Down
55 changes: 25 additions & 30 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,11 @@ dev = [
"black >= 23.7.0",
"flake8 >= 5.0.1",
"pyproject-flake8 >= 5.0.0",
"mypy >= 1.0.0",
"ipykernel >= 6.24.0",
"pytest-cov >= 4.1.0",
"pytest-mock >= 3.6.1",
"poethepoet >= 0.24.0",
"qm-saas",
]
docs = ["mkdocstrings[python]>=0.18", "mkdocs-gen-files", "mkdocs-jupyter"]
Expand Down Expand Up @@ -101,6 +103,9 @@ preview = true

[tool.flake8]
max-line-length = 88
# Black handles line length; E501 fires on lines it can't shorten (type: ignore comments, docstrings).
extend-ignore = ["E501"]
per-file-ignores = ["**/__init__.py:F401,F403,F405"]

[tool.setuptools]
packages = ["quam"]
Expand All @@ -112,38 +117,28 @@ local_scheme = "no-local-version"
[tool.uv]
prerelease = "allow"

# Previous quam settings
[tool.poe.tasks.format]
cmd = "black quam/"
help = "Auto-format source files"

### Extra required dependencies
# "jsonschema = ^4.14.0",
# "pydantic = ^1.9.2",
# "rich = ^12.5.1",
# "rich-click = ^1.6.1",
# "pyyaml = ^6.0",
[tool.poe.tasks.check-format]
cmd = "black --check quam/"
help = "Check formatting without modifying files"

### Extra optional dependencies
# flake8-bugbear = "^22.4.25"
# poethepoet = "^0.10.0"
# typing-extensions = "^4.3.0"
# coverage = "^6.4.4"
[tool.poe.tasks.lint]
cmd = "pflake8 quam/"
help = "Run flake8 linting"

[tool.poe.tasks.typecheck]
# --ignore-missing-imports: suppress errors for deps without type stubs (e.g. qm-qua).
# --follow-imports=silent: collect type info from followed imports but suppress errors inside them.
cmd = "mypy quam/ --ignore-missing-imports --follow-imports=silent"
help = "Run mypy type checking"

# [tool.poe.tasks.check-format]
# cmd = "black . --check --line-length 80"
# help = "Format source files according to the style rules"
[tool.poe.tasks.test]
cmd = "pytest"
help = "Run all unit tests"

# [tool.poe.tasks.format]
# cmd = "black . --line-length 80"
# help = "Format source files according to the style rules"

# [tool.poe.tasks.lint]
# cmd = "flake8 ."
# help = "Check for lint errors"

# [tool.poe.tasks.test]
# cmd = "pytest"
# help = "Run all unit tests"

# [tool.poe.tasks.check]
# sequence = ["check-format", "lint", "test"]
# help = "Run format and all checks on the code"
[tool.poe.tasks.check]
sequence = ["check-format", "lint", "typecheck", "test"]
help = "Run all checks (format, lint, types, tests)"
1 change: 1 addition & 0 deletions quam/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .quantum_components import *
from . import macro
from quam.config import get_quam_config
from . import basic_quam, hardware, channels, octave, quantum_components

__all__ = [
*basic_quam.__all__,
Expand Down
128 changes: 103 additions & 25 deletions quam/components/_waveform_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,35 @@


def drag_gaussian_pulse_waveforms(
amplitude, length, sigma, alpha, anharmonicity, detuning=0.0, subtracted=True, sampling_rate=1e9, **kwargs
amplitude,
length,
sigma,
alpha,
anharmonicity,
detuning=0.0,
subtracted=True,
sampling_rate=1e9,
**kwargs,
):
if alpha != 0 and anharmonicity == 0:
raise ValueError("Cannot create a DRAG pulse with `anharmonicity=0`")
t = np.arange(length, step=1e9 / sampling_rate)
center = (length - 1e9 / sampling_rate) / 2
gauss_wave = amplitude * np.exp(-((t - center) ** 2) / (2 * sigma**2))
gauss_der_wave = (
amplitude * (-2 * 1e9 * (t - center) / (2 * sigma**2)) * np.exp(-((t - center) ** 2) / (2 * sigma**2))
amplitude
* (-2 * 1e9 * (t - center) / (2 * sigma**2))
* np.exp(-((t - center) ** 2) / (2 * sigma**2))
)
if subtracted:
gauss_wave = gauss_wave - gauss_wave[-1]
z = gauss_wave + 1j * 0
if anharmonicity != detuning:
z += 1j * gauss_der_wave * (alpha / (2 * np.pi * anharmonicity - 2 * np.pi * detuning))
z += (
1j
* gauss_der_wave
* (alpha / (2 * np.pi * anharmonicity - 2 * np.pi * detuning))
)
elif alpha != 0:
raise ValueError(
"Cannot create DRAG waveform if anharmonicity == detuning and alpha != 0."
Expand All @@ -31,16 +45,25 @@ def drag_gaussian_pulse_waveforms(
return z.real.tolist(), z.imag.tolist()


def drag_cosine_pulse_waveforms(amplitude, length, alpha, anharmonicity, detuning=0.0, sampling_rate=1e9, **kwargs):
def drag_cosine_pulse_waveforms(
amplitude, length, alpha, anharmonicity, detuning=0.0, sampling_rate=1e9, **kwargs
):
if alpha != 0 and anharmonicity == 0:
raise ValueError("Cannot create a DRAG pulse with `anharmonicity=0`")
t = np.arange(length, step=1e9 / sampling_rate)
end_point = length - 1e9 / sampling_rate
cos_wave = 0.5 * amplitude * (1 - np.cos(t * 2 * np.pi / end_point))
sin_wave = 0.5 * amplitude * (2 * np.pi / end_point * 1e9) * np.sin(t * 2 * np.pi / end_point)
sin_wave = (
0.5
* amplitude
* (2 * np.pi / end_point * 1e9)
* np.sin(t * 2 * np.pi / end_point)
)
z = cos_wave + 1j * 0
if anharmonicity != detuning:
z += 1j * sin_wave * (alpha / (2 * np.pi * anharmonicity - 2 * np.pi * detuning))
z += (
1j * sin_wave * (alpha / (2 * np.pi * anharmonicity - 2 * np.pi * detuning))
)
elif alpha != 0:
raise ValueError(
"Cannot create DRAG waveform if anharmonicity == detuning and alpha != 0."
Expand All @@ -49,56 +72,97 @@ def drag_cosine_pulse_waveforms(amplitude, length, alpha, anharmonicity, detunin
return z.real.tolist(), z.imag.tolist()


def flattop_gaussian_waveform(amplitude, flat_length, rise_fall_length, return_part="all", sampling_rate=1e9):
assert sampling_rate % 1e9 == 0, "The sampling rate must be an integer multiple of 1e9 samples per second."
def flattop_gaussian_waveform(
amplitude, flat_length, rise_fall_length, return_part="all", sampling_rate=1e9
):
assert (
sampling_rate % 1e9 == 0
), "The sampling rate must be an integer multiple of 1e9 samples per second."
gauss_wave = amplitude * gaussian(
int(np.round(2 * rise_fall_length * sampling_rate / 1e9)),
rise_fall_length / 5 * sampling_rate / 1e9,
)
rise_part = gauss_wave[: int(rise_fall_length * sampling_rate / 1e9)].tolist()
if return_part == "all":
return rise_part + [amplitude] * int(flat_length * sampling_rate / 1e9) + rise_part[::-1]
return (
rise_part
+ [amplitude] * int(flat_length * sampling_rate / 1e9)
+ rise_part[::-1]
)
elif return_part == "rise":
return rise_part
elif return_part == "fall":
return rise_part[::-1]
raise ValueError("'return_part' must be 'all', 'rise', or 'fall'")


def flattop_cosine_waveform(amplitude, flat_length, rise_fall_length, return_part="all", sampling_rate=1e9):
assert sampling_rate % 1e9 == 0, "The sampling rate must be an integer multiple of 1e9 samples per second."
def flattop_cosine_waveform(
amplitude, flat_length, rise_fall_length, return_part="all", sampling_rate=1e9
):
assert (
sampling_rate % 1e9 == 0
), "The sampling rate must be an integer multiple of 1e9 samples per second."
rise_part = (
amplitude * 0.5 * (1 - np.cos(np.linspace(0, np.pi, int(rise_fall_length * sampling_rate / 1e9))))
amplitude
* 0.5
* (
1
- np.cos(np.linspace(0, np.pi, int(rise_fall_length * sampling_rate / 1e9)))
)
).tolist()
if return_part == "all":
return rise_part + [amplitude] * int(flat_length * sampling_rate / 1e9) + rise_part[::-1]
return (
rise_part
+ [amplitude] * int(flat_length * sampling_rate / 1e9)
+ rise_part[::-1]
)
elif return_part == "rise":
return rise_part
elif return_part == "fall":
return rise_part[::-1]
raise ValueError("'return_part' must be 'all', 'rise', or 'fall'")


def flattop_tanh_waveform(amplitude, flat_length, rise_fall_length, return_part="all", sampling_rate=1e9):
assert sampling_rate % 1e9 == 0, "The sampling rate must be an integer multiple of 1e9 samples per second."
def flattop_tanh_waveform(
amplitude, flat_length, rise_fall_length, return_part="all", sampling_rate=1e9
):
assert (
sampling_rate % 1e9 == 0
), "The sampling rate must be an integer multiple of 1e9 samples per second."
rise_part = (
amplitude * 0.5 * (1 + np.tanh(np.linspace(-4, 4, int(rise_fall_length * sampling_rate / 1e9))))
amplitude
* 0.5
* (1 + np.tanh(np.linspace(-4, 4, int(rise_fall_length * sampling_rate / 1e9))))
).tolist()
if return_part == "all":
return rise_part + [amplitude] * int(flat_length * sampling_rate / 1e9) + rise_part[::-1]
return (
rise_part
+ [amplitude] * int(flat_length * sampling_rate / 1e9)
+ rise_part[::-1]
)
elif return_part == "rise":
return rise_part
elif return_part == "fall":
return rise_part[::-1]
raise ValueError("'return_part' must be 'all', 'rise', or 'fall'")


def flattop_blackman_waveform(amplitude, flat_length, rise_fall_length, return_part="all", sampling_rate=1e9):
assert sampling_rate % 1e9 == 0, "The sampling rate must be an integer multiple of 1e9 samples per second."
blackman_wave = amplitude * blackman(2 * int(rise_fall_length * sampling_rate / 1e9))
def flattop_blackman_waveform(
amplitude, flat_length, rise_fall_length, return_part="all", sampling_rate=1e9
):
assert (
sampling_rate % 1e9 == 0
), "The sampling rate must be an integer multiple of 1e9 samples per second."
blackman_wave = amplitude * blackman(
2 * int(rise_fall_length * sampling_rate / 1e9)
)
rise_part = blackman_wave[: int(rise_fall_length * sampling_rate / 1e9)].tolist()
if return_part == "all":
return rise_part + [amplitude] * int(flat_length * sampling_rate / 1e9) + rise_part[::-1]
return (
rise_part
+ [amplitude] * int(flat_length * sampling_rate / 1e9)
+ rise_part[::-1]
)
elif return_part == "rise":
return rise_part
elif return_part == "fall":
Expand All @@ -107,7 +171,9 @@ def flattop_blackman_waveform(amplitude, flat_length, rise_fall_length, return_p


def blackman_integral_waveform(pulse_length, v_start, v_end, sampling_rate=1e9):
assert sampling_rate % 1e9 == 0, "The sampling rate must be an integer multiple of 1e9 samples per second."
assert (
sampling_rate % 1e9 == 0
), "The sampling rate must be an integer multiple of 1e9 samples per second."
time = np.linspace(0, pulse_length - 1, int(pulse_length * sampling_rate / 1e9))
waveform = v_start + (
time / (pulse_length - 1)
Expand All @@ -131,15 +197,27 @@ def compress_integration_weights(integration_weights, N=100):
integration_weights[idx, 0] = (w1 * t1 + w2 * t2) / (t1 + t2)
integration_weights[idx, 1] = t1 + t2
integration_weights = np.delete(integration_weights, idx + 1, 0)
return list(zip(integration_weights.T[0].tolist(), integration_weights.T[1].astype(int).tolist()))
return list(
zip(
integration_weights.T[0].tolist(),
integration_weights.T[1].astype(int).tolist(),
)
)


def convert_integration_weights(integration_weights, N=100, accuracy=2**-15):
integration_weights = _round_to_fixed_point_accuracy(np.array(integration_weights), accuracy)
integration_weights = _round_to_fixed_point_accuracy(
np.array(integration_weights), accuracy
)
changes_indices = np.where(np.abs(np.diff(integration_weights)) > 0)[0].tolist()
prev_index = -1
new_weights = []
for curr_index in changes_indices + [len(integration_weights) - 1]:
new_weights.append((integration_weights[curr_index].tolist(), round(4 * (curr_index - prev_index))))
new_weights.append(
(
integration_weights[curr_index].tolist(),
round(4 * (curr_index - prev_index)),
)
)
prev_index = curr_index
return compress_integration_weights(new_weights, N=N)
Loading
Loading