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 @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
### Fixed

- `add_default_transmon_pair_macros` now passes `flux_pulse_qubit="const"` (was `flux_pulse_control="const"`) to `CZGate`, matching the field rename introduced in v0.4.0 and aligning with the `"const"` pulse added to every `FluxTunableTransmon` Z line by `add_default_transmon_pulses`.
- Applied black formatting across the entire repo.

## [0.4.0] - 2026-05-26

Expand Down
2 changes: 1 addition & 1 deletion pylint_qua_plugin/INTEGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,4 +342,4 @@ If pre-commit isn't finding the plugin:

## License

Apache-2.0
Apache-2.0
2 changes: 1 addition & 1 deletion pylint_qua_plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,4 @@ if_(n_op & 1 == 0) # pylint: disable=C1805

## License

Apache-2.0
Apache-2.0
2 changes: 1 addition & 1 deletion pylint_qua_plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
from .pylint_qua_plugin import register

__version__ = "0.1.0"
__all__ = ["register"]
__all__ = ["register"]
152 changes: 102 additions & 50 deletions pylint_qua_plugin/pylint_qua_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@

Usage:
1. Add to your pylint configuration:

# pyproject.toml
[tool.pylint.main]
load-plugins = ["pylint_qua_plugin"]

# Or .pylintrc
[MAIN]
load-plugins=pylint_qua_plugin
Expand All @@ -25,7 +25,7 @@

Suppressed rules:
- C1805: use-implicit-booleaness-not-comparison-to-zero
- C1803: use-implicit-booleaness-not-comparison-to-string
- C1803: use-implicit-booleaness-not-comparison-to-string
- C0121: singleton-comparison
- R1714: consider-using-in
- W0104: pointless-statement
Expand All @@ -45,11 +45,19 @@
from astroid import nodes, MANAGER
from pylint.lint import PyLinter


# Rules that conflict with QUA DSL semantics (both codes and symbols)
QUA_SUPPRESSED_MSGIDS: Set[str] = {
# Message codes (uppercase)
"C1805", "C1803", "C0121", "R1714", "W0104", "W0106", "R1705", "R1720", "R1709", "W0127",
"C1805",
"C1803",
"C0121",
"R1714",
"W0104",
"W0106",
"R1705",
"R1720",
"R1709",
"W0127",
# Symbolic names (pylint uses these internally)
"use-implicit-booleaness-not-comparison-to-zero",
"use-implicit-booleaness-not-comparison-to-string",
Expand All @@ -67,44 +75,83 @@
# These can appear outside of `with qua.program()` blocks in helper functions
QUA_CONTROL_FLOW_FUNCTIONS: Set[str] = {
# Control flow
"if_", "else_", "elif_",
"for_", "for_each_",
"if_",
"else_",
"elif_",
"for_",
"for_each_",
"while_",
"switch_", "case_", "default_",
"switch_",
"case_",
"default_",
# Variable operations
"assign", "assign_addition_", "assign_subtraction_",
"declare", "declare_stream",
"assign",
"assign_addition_",
"assign_subtraction_",
"declare",
"declare_stream",
# Timing and synchronization
"wait", "wait_for_trigger", "align", "reset_phase", "reset_frame", "reset_global_phase",
"frame_rotation", "frame_rotation_2pi", "update_frequency",
"wait",
"wait_for_trigger",
"align",
"reset_phase",
"reset_frame",
"reset_global_phase",
"frame_rotation",
"frame_rotation_2pi",
"update_frequency",
# Playback
"play", "measure", "save", "pause",
"ramp", "ramp_to_zero",
"play",
"measure",
"save",
"pause",
"ramp",
"ramp_to_zero",
# Math operations that take QUA variables
"Math.abs", "Math.log", "Math.log2", "Math.log10", "Math.exp", "Math.sqrt",
"Math.pow", "Math.sin", "Math.cos", "Math.tan", "Math.asin", "Math.acos", "Math.atan",
"Math.div", "Math.msb", "Math.sum", "Math.max", "Math.min",
"Cast.to_int", "Cast.to_fixed", "Cast.to_bool", "Cast.unsafe_cast_int", "Cast.unsafe_cast_fixed",
"Math.abs",
"Math.log",
"Math.log2",
"Math.log10",
"Math.exp",
"Math.sqrt",
"Math.pow",
"Math.sin",
"Math.cos",
"Math.tan",
"Math.asin",
"Math.acos",
"Math.atan",
"Math.div",
"Math.msb",
"Math.sum",
"Math.max",
"Math.min",
"Cast.to_int",
"Cast.to_fixed",
"Cast.to_bool",
"Cast.unsafe_cast_int",
"Cast.unsafe_cast_fixed",
"Util.cond",
# I/O
"set_dc_offset", "get_dc_offset",
"IO1", "IO2",
"set_dc_offset",
"get_dc_offset",
"IO1",
"IO2",
# Randomization
"Random.rand_int", "Random.rand_fixed",
"Random.rand_int",
"Random.rand_fixed",
}

# Also match these as bare function names (without module prefix)
QUA_CONTROL_FLOW_BARE_NAMES: Set[str] = {
name.split(".")[-1] for name in QUA_CONTROL_FLOW_FUNCTIONS
} | {
name for name in QUA_CONTROL_FLOW_FUNCTIONS if "." not in name
}
} | {name for name in QUA_CONTROL_FLOW_FUNCTIONS if "." not in name}


def _is_qua_context_call(node: nodes.Call) -> bool:
"""Check if a Call node represents a QUA program context manager."""
func = node.func

if isinstance(func, nodes.Attribute):
if func.attrname != "program":
return False
Expand All @@ -118,21 +165,21 @@ def _is_qua_context_call(node: nodes.Call) -> bool:
parts.insert(0, current.name)
module_path = ".".join(parts)
return "qua" in module_path.lower()

elif isinstance(func, nodes.Name):
return func.name == "program"

return False


def _is_qua_control_flow_call(node: nodes.Call) -> bool:
"""Check if a Call node is a QUA control flow function."""
func = node.func

# Handle bare function names: if_(condition)
if isinstance(func, nodes.Name):
return func.name in QUA_CONTROL_FLOW_BARE_NAMES

# Handle attribute access: qua.if_(condition), Math.abs(x)
if isinstance(func, nodes.Attribute):
# Build the full call path
Expand All @@ -143,18 +190,18 @@ def _is_qua_control_flow_call(node: nodes.Call) -> bool:
current = current.expr
if isinstance(current, nodes.Name):
parts.insert(0, current.name)

full_path = ".".join(parts)

# Check if it matches any QUA function pattern
# Match: Math.abs, Cast.to_int, qua.if_, etc.
if full_path in QUA_CONTROL_FLOW_FUNCTIONS:
return True

# Also match if the last part is a QUA function (e.g., qm.qua.if_)
if func.attrname in QUA_CONTROL_FLOW_BARE_NAMES:
return True

return False


Expand All @@ -168,18 +215,18 @@ def _get_call_line_range(node: nodes.Call) -> Tuple[int, int]:
def _collect_qua_ranges(module: nodes.Module) -> List[Tuple[int, int]]:
"""Collect all line ranges inside QUA contexts and QUA control flow calls."""
ranges = []

# Collect `with qua.program()` blocks
for node in module.nodes_of_class(nodes.With):
for context_item, _ in node.items:
if isinstance(context_item, nodes.Call) and _is_qua_context_call(context_item):
ranges.append((node.lineno, node.end_lineno or node.lineno))

# Collect QUA control flow function calls (can be anywhere in the file)
for node in module.nodes_of_class(nodes.Call):
if _is_qua_control_flow_call(node):
ranges.append(_get_call_line_range(node))

return ranges


Expand All @@ -196,8 +243,8 @@ def _in_qua_range(line: int, ranges: List[Tuple[int, int]]) -> bool:
def _module_transform(module: nodes.Module) -> nodes.Module:
"""AST transform that collects QUA ranges before any checking happens."""
global _qua_ranges
filepath = getattr(module, 'file', None)
Comment thread
JacobFastQM marked this conversation as resolved.
if filepath and filepath != '<?>':
filepath = getattr(module, "file", None)
if filepath and filepath != "<?>":
ranges = _collect_qua_ranges(module)
if ranges:
_qua_ranges[filepath] = ranges
Expand All @@ -208,15 +255,15 @@ def register(linter: PyLinter) -> None:
"""Register the QUA plugin with pylint."""
global _qua_ranges, _transform_registered
_qua_ranges = {}

# Register the AST transform (only once globally)
if not _transform_registered:
MANAGER.register_transform(nodes.Module, _module_transform)
_transform_registered = True

# Wrap add_message to filter QUA-incompatible messages
original_add_message = linter.add_message

@functools.wraps(original_add_message)
def filtered_add_message(
msgid: str,
Expand All @@ -230,14 +277,14 @@ def filtered_add_message(
) -> None:
# Get line number
check_line = line if line is not None else (node.lineno if node else None)

# Get filepath - try multiple sources
filepath = getattr(linter, 'current_file', None)
filepath = getattr(linter, "current_file", None)
if not filepath and node is not None:
# Try to get file from the node's root
root = node.root()
filepath = getattr(root, 'file', None)
filepath = getattr(root, "file", None)

# Check if should suppress
if (
check_line is not None
Expand All @@ -247,13 +294,18 @@ def filtered_add_message(
and _in_qua_range(check_line, _qua_ranges[filepath])
):
return # Suppress this message

return original_add_message(
msgid, line=line, node=node, args=args,
confidence=confidence, col_offset=col_offset,
end_lineno=end_lineno, end_col_offset=end_col_offset,
msgid,
line=line,
node=node,
args=args,
confidence=confidence,
col_offset=col_offset,
end_lineno=end_lineno,
end_col_offset=end_col_offset,
)

linter.add_message = filtered_add_message


Expand Down
1 change: 0 additions & 1 deletion quam_builder/architecture/nv_center/components/laser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from quam.core import QuamComponent, quam_dataclass
from quam.components.channels import Channel, SingleChannel


__all__ = ["LaserControl", "LaserLFAnalog", "LaserLFDigital"]


Expand Down
1 change: 0 additions & 1 deletion quam_builder/architecture/nv_center/components/spcm.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from quam.core import quam_dataclass
from quam.components.channels import InOutSingleChannel


__all__ = ["SPCM"]


Expand Down
9 changes: 2 additions & 7 deletions quam_builder/architecture/nv_center/components/xy_drive.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
get_output_power_iq_channel,
)


__all__ = ["XYDriveIQ", "XYDriveMW"]


Expand All @@ -22,9 +21,7 @@ class XYDriveBase:
"""

@staticmethod
def calculate_voltage_scaling_factor(
fixed_power_dBm: float, target_power_dBm: float
):
def calculate_voltage_scaling_factor(fixed_power_dBm: float, target_power_dBm: float):
"""
Calculate the voltage scaling factor required to scale fixed power to target power.

Expand Down Expand Up @@ -92,9 +89,7 @@ def set_output_power(
ValueError: If `gain` or `amplitude` is outside their valid ranges.

"""
return set_output_power_iq_channel(
self, power_in_dbm, gain, max_amplitude, Z, operation
)
return set_output_power_iq_channel(self, power_in_dbm, gain, max_amplitude, Z, operation)


@quam_dataclass
Expand Down
8 changes: 2 additions & 6 deletions quam_builder/architecture/nv_center/qpu/base_quam.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,7 @@ def get_serialiser(cls) -> JSONSerialiser:

This method can be overridden by subclasses to provide a custom serialiser.
"""
return JSONSerialiser(
content_mapping={"wiring": "wiring.json", "network": "wiring.json"}
)
return JSONSerialiser(content_mapping={"wiring": "wiring.json", "network": "wiring.json"})

def get_octave_config(self) -> QmOctaveConfig:
"""Return the Octave configuration."""
Expand Down Expand Up @@ -109,9 +107,7 @@ def calibrate_octave_ports(self, QM: QuantumMachine) -> None:
try:
self.qubits[name].calibrate_octave(QM)
except NoCalibrationElements:
print(
f"No calibration elements found for {name}. Skipping calibration."
)
print(f"No calibration elements found for {name}. Skipping calibration.")

@property
def active_qubits(self) -> List[NVCenter]:
Expand Down
Loading
Loading