Skip to content
Open
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 @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- A custom QMM class can be specified in the network configuration, and enabled/disabled with the `use_custom_qmm` flag.
### Fixed
- NV center - fix invalid `SPCM` component.
- Quantum Dots - fix for voltage_sequence with amplified and attenuated `VoltageGates`

## [0.2.0] - 2025-10-29
### Added
Expand Down
54 changes: 43 additions & 11 deletions quam_builder/tools/voltage_sequence/voltage_sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,9 @@ def _play_step_on_channel(
):
"""Plays a scaled step on a single channel."""
DEFAULT_WF_AMPLITUDE = channel.operations[DEFAULT_PULSE_NAME].amplitude
DEFAULT_AMPLITUDE_BITSHIFT = int(np.log2(1 / DEFAULT_WF_AMPLITUDE))
MIN_PULSE_DURATION_NS = channel.operations[DEFAULT_PULSE_NAME].length
inv_wf_amplitude = float(np.round(1.0 / DEFAULT_WF_AMPLITUDE, 10))
log2_inv_wf = np.log2(1.0 / DEFAULT_WF_AMPLITUDE)

if self.gate_set.adjust_for_attenuation:
delta_v = self._adjust_for_attenuation(channel, delta_v)
Expand All @@ -241,9 +242,14 @@ def _play_step_on_channel(
return

if is_qua_type(delta_v):
scaled_amp = delta_v << DEFAULT_AMPLITUDE_BITSHIFT
# Bit-shift only matches scaling when 1/DEFAULT_WF_AMPLITUDE is a power of two
# (e.g. 0.25 V baseband). Amplified LF-FEM uses 1.25 V → use explicit multiply.
if log2_inv_wf == int(log2_inv_wf) and int(log2_inv_wf) >= 0:
scaled_amp = delta_v << int(log2_inv_wf)
else:
scaled_amp = delta_v * inv_wf_amplitude
else:
scaled_amp = np.round(delta_v * (1.0 / (DEFAULT_WF_AMPLITUDE)), 10)
scaled_amp = np.round(delta_v * inv_wf_amplitude, 10)
duration_cycles = duration >> 2 # Convert ns to clock cycles

if is_qua_type(duration):
Expand Down Expand Up @@ -727,8 +733,6 @@ def apply_compensation_pulse(
self._common_voltages_change(target_voltages_dict=zero_dict, duration=16)

for ch_name, channel_obj in self.gate_set.channels.items():
DEFAULT_WF_AMPLITUDE = channel_obj.operations[DEFAULT_PULSE_NAME].amplitude
DEFAULT_AMPLITUDE_BITSHIFT = int(np.log2(1 / DEFAULT_WF_AMPLITUDE))
channel_limit = self._channel_max_voltage[ch_name]
channel_max_voltage = min(max_voltage, channel_limit)
if max_voltage > channel_limit:
Expand Down Expand Up @@ -843,7 +847,9 @@ def ramp_to_zero(

Args:
ramp_duration: Optional. The duration (ns) of the ramp to zero.
If None, QUA's `ramp_to_zero` command is used for an immediate ramp.
If None, uses QUA's ``ramp_to_zero`` on each element when
``adjust_for_attenuation`` is off; when it is on, uses an explicit
ramp so OPX scaling matches ``step_to_voltages`` / ``ramp_to_voltages``.
Must be >16ns and a multiple of 4ns. Can be a fixed value or a QUA
variable.
reset_tracker: Optional. Reset integrated voltage tracking
Expand All @@ -863,12 +869,38 @@ def ramp_to_zero(
"""

if ramp_duration is None:
for ch_name, channel_obj in self.gate_set.channels.items():
tracker = self.state_trackers[ch_name]
ramp_to_zero(channel_obj.name)
tracker.update_integrated_voltage(
level=0.0, duration=0, ramp_duration=channel_obj.sticky.duration
if self.gate_set.adjust_for_attenuation:
# QUA ramp_to_zero() does not apply the same OPX scaling as _play_*_on_channel
# when adjust_for_attenuation is enabled; use an explicit ramp so attenuation
# and default-pulse amplitude stay consistent (e.g. amplified LF-FEM).
sticky_durations = [
int(sticky.duration)
for sticky in (
getattr(ch, "sticky", None) for ch in self.gate_set.channels.values()
)
if sticky is not None and getattr(sticky, "duration", None) is not None
]
ramp_ns = max(sticky_durations) if sticky_durations else MIN_PULSE_DURATION_NS
self.ramp_to_voltages(
voltages={ch_name: 0.0 for ch_name in self.gate_set.channels},
duration=0,
ramp_duration=ramp_ns,
)
if not self._track_integrated_voltage:
for ch_name, channel_obj in self.gate_set.channels.items():
tracker = self.state_trackers[ch_name]
tracker.update_integrated_voltage(
level=0.0,
duration=0,
ramp_duration=channel_obj.sticky.duration,
)
else:
for ch_name, channel_obj in self.gate_set.channels.items():
tracker = self.state_trackers[ch_name]
ramp_to_zero(channel_obj.name)
tracker.update_integrated_voltage(
level=0.0, duration=0, ramp_duration=channel_obj.sticky.duration
)

else:
self.ramp_to_voltages(
Expand Down
72 changes: 72 additions & 0 deletions tests_against_server/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Shared fixtures for tests that run against a QM simulator (on-prem or cloud)."""

from __future__ import annotations

import json
from pathlib import Path

import pytest

pytest.importorskip("qm_saas")
from qm import QuantumMachinesManager
from qm_saas import ClusterConfig, QmSaas

_REPO_ROOT = Path(__file__).resolve().parents[1]
_SAAS_CREDENTIALS_PATH = _REPO_ROOT / ".qm_saas_credentials.json"


def _saas_cluster_config() -> ClusterConfig:
"""OPX1000 layout matching voltage_gate_sequence LF-FEM ports on slot 5."""
cluster_config = ClusterConfig()
cluster_config.controller().lf_fems(5)
return cluster_config


@pytest.fixture(scope="session")
def qm_saas_credentials() -> dict[str, str]:
if not _SAAS_CREDENTIALS_PATH.exists():
pytest.skip(
f"QM SaaS credentials not found at {_SAAS_CREDENTIALS_PATH}. "
"Copy .qm_saas_credentials.json.example to .qm_saas_credentials.json "
"and fill in your credentials."
)
with open(_SAAS_CREDENTIALS_PATH) as f:
return json.load(f)


@pytest.fixture(scope="session")
def qm_saas_client(qm_saas_credentials: dict[str, str]):
client = QmSaas(
email=qm_saas_credentials["email"],
password=qm_saas_credentials["password"],
host=qm_saas_credentials.get("host", "qm-saas.dev.quantum-machines.co"),
)
client.close_all()
yield client
client.close_all()


@pytest.fixture(scope="session")
def qm_saas_instance(qm_saas_client: QmSaas):
cluster_config = _saas_cluster_config()
instance = qm_saas_client.simulator(qm_saas_client.latest_version(), cluster_config)
instance.spawn()
if not instance.is_alive:
pytest.fail(
f"QM SaaS simulator instance failed to start (expires_at={instance.expires_at})"
)
yield instance
instance.close()


@pytest.fixture(scope="session")
def qmm_saas(qm_saas_instance):
if not qm_saas_instance.is_alive:
pytest.fail(
f"QM SaaS simulator instance is no longer alive (expires_at={qm_saas_instance.expires_at})"
)
return QuantumMachinesManager(
host=qm_saas_instance.host,
port=qm_saas_instance.port,
connection_headers=qm_saas_instance.default_connection_headers,
)
37 changes: 34 additions & 3 deletions tests_against_server/voltage_gate_sequence/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
VirtualizationLayer,
VoltageGate,
)
from quam_builder.builder.quantum_dots.pulses import add_default_baseband_pulse
from qm import QuantumMachinesManager
import numpy as np

from quam.components import pulses


@quam_dataclass
class QuamGateSet(QuamRoot):
Expand All @@ -37,17 +40,42 @@ def machine():
machine = QuamGateSet(
gate_set=GateSet(
id="test_gate_set",
channels={
"ch1": VoltageGate(
opx_output=LFFEMAnalogOutputPort("con1", 5, 6, upsampling_mode="pulse"),
sticky=StickyChannelAddon(duration=100, digital=False),
attenuation=10,
),
"ch2": VoltageGate(
opx_output=LFFEMAnalogOutputPort("con1", 5, 3, upsampling_mode="pulse"),
sticky=StickyChannelAddon(duration=100, digital=False),
attenuation=10,
),
},
adjust_for_attenuation=True,
),
)
for channel in machine.gate_set.channels.values():
add_default_baseband_pulse(channel)
return machine


@pytest.fixture
def machine_amplified():
machine = QuamGateSet(
gate_set=GateSet(
id="test_gate_set_amplified",
channels={
"ch1": VoltageGate(
opx_output=LFFEMAnalogOutputPort(
"con1", 5, 6, upsampling_mode="pulse"
"con1", 5, 6, upsampling_mode="pulse", output_mode="amplified"
),
sticky=StickyChannelAddon(duration=100, digital=False),
attenuation=10,
),
"ch2": VoltageGate(
opx_output=LFFEMAnalogOutputPort(
"con1", 5, 3, upsampling_mode="pulse"
"con1", 5, 3, upsampling_mode="pulse", output_mode="amplified"
),
sticky=StickyChannelAddon(duration=100, digital=False),
attenuation=10,
Expand All @@ -56,6 +84,8 @@ def machine():
adjust_for_attenuation=True,
),
)
for channel in machine.gate_set.channels.values():
add_default_baseband_pulse(channel)
return machine


Expand Down Expand Up @@ -92,5 +122,6 @@ def virtual_machine():
)
)

# machine.virtual_gate_set = gate_set
for channel in machine.virtual_gate_set.channels.values():
add_default_baseband_pulse(channel)
return machine
Loading