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
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)

## [Unreleased]

### Added
- wirer - Support for **QDAC-II (QDAC2)** in registration, DC channel allocation, and visualization:
- Instrument id `qdac2`; `Instruments.add_qdac2(indices)` exposes 24 DC outputs and 4 digital trigger inputs per unit on `available_channels`.
- Channel types `InstrumentChannelQdac2Output` and `InstrumentChannelQdac2DigitalInput`; constants `NUM_QDAC2_OUTPUT_PORTS` and `NUM_QDAC2_DIGITAL_INPUT_PORTS`.
- `qdac2_spec` (`ChannelSpecQdac2`) for DC voltage gates with optional external trigger input on the same QDAC2 unit; exported from `qualang_tools.wirer`.
- `allocate_dc_channels` allocates QDAC2-only lines and, when wiring constraints combine LF-FEM with QDAC2 or OPX+ with QDAC2, tries additional dual-instrument masks so each element gets the corresponding pair of channels.
- Visualizer: QDAC2 figure (3×8 DC grid and four trigger inputs) with port positions and annotations.

## [0.22.0] - 2026-04-01
### Added
Expand Down
2 changes: 2 additions & 0 deletions qualang_tools/wirer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
opx_iq_spec,
opx_iq_octave_spec,
octave_spec,
qdac2_spec,
)
from .wirer.wirer_exceptions import NotEnoughPulsersException, NotEnoughChannelsException

Expand All @@ -29,4 +30,5 @@
"opx_iq_spec",
"opx_iq_octave_spec",
"octave_spec",
"qdac2_spec",
]
2 changes: 2 additions & 0 deletions qualang_tools/wirer/instruments/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@
NUM_MW_FEM_DIGITAL_OUTPUT_PORTS = 8
NUM_THREADS_PER_FEM = 16
NUM_THREADS_PER_OPX_PLUS = 18
NUM_QDAC2_OUTPUT_PORTS = 24
NUM_QDAC2_DIGITAL_INPUT_PORTS = 4
23 changes: 22 additions & 1 deletion qualang_tools/wirer/instruments/instrument_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class InstrumentChannelAnalog:
signal_type = "analog"


InstrumentIdType = Literal["lf-fem", "mw-fem", "opx+", "octave", "external-mixer"]
InstrumentIdType = Literal["lf-fem", "mw-fem", "opx+", "octave", "external-mixer", "qdac2"]


@dataclass(eq=False)
Expand Down Expand Up @@ -67,6 +67,11 @@ class InstrumentChannelExternalMixer:
instrument_id: InstrumentIdType = "external-mixer"


@dataclass(eq=False)
class InstrumentChannelQdac2:
instrument_id: InstrumentIdType = "qdac2"


@dataclass(eq=False)
class InstrumentChannelLfFemInput(
InstrumentChannelAnalog, InstrumentChannelLfFem, InstrumentChannelInput, InstrumentChannel
Expand Down Expand Up @@ -172,6 +177,20 @@ class InstrumentChannelExternalMixerDigitalInput(
pass


@dataclass(eq=False)
class InstrumentChannelQdac2Output(
InstrumentChannelAnalog, InstrumentChannelQdac2, InstrumentChannelOutput, InstrumentChannel
):
pass


@dataclass(eq=False)
class InstrumentChannelQdac2DigitalInput(
InstrumentChannelDigital, InstrumentChannelQdac2, InstrumentChannelInput, InstrumentChannel
):
pass


AnyInstrumentChannel = Union[
InstrumentChannelLfFemInput,
InstrumentChannelLfFemOutput,
Expand All @@ -183,4 +202,6 @@ class InstrumentChannelExternalMixerDigitalInput(
InstrumentChannelOctaveOutput,
InstrumentChannelExternalMixerInput,
InstrumentChannelExternalMixerOutput,
InstrumentChannelQdac2Output,
InstrumentChannelQdac2DigitalInput,
]
23 changes: 23 additions & 0 deletions qualang_tools/wirer/instruments/instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
InstrumentChannelExternalMixerInput,
InstrumentChannelExternalMixerOutput,
InstrumentChannelExternalMixerDigitalInput,
InstrumentChannelQdac2Output,
InstrumentChannelQdac2DigitalInput,
)
from .instrument_channels import *
from .instrument_pulsers import *
Expand Down Expand Up @@ -47,6 +49,27 @@ def add_external_mixer(self, indices: Union[List[int], int]):
channel = InstrumentChannelExternalMixerDigitalInput(con=index, port=1)
self.available_channels.add(channel)

def add_qdac2(self, indices: Union[List[int], int]):
"""
Add QDAC2 instruments to the available channels.

Each unit exposes all DC analog output ports and external trigger input ports.

Args:
indices (List[int] | int): One or more QDAC2 unit indices (e.g. as used in the lab stack).
"""
if isinstance(indices, int):
indices = [indices]

for index in indices:
for port in range(1, NUM_QDAC2_OUTPUT_PORTS + 1):
channel = InstrumentChannelQdac2Output(con=index, port=port)
self.available_channels.add(channel)

for port in range(1, NUM_QDAC2_DIGITAL_INPUT_PORTS + 1):
channel = InstrumentChannelQdac2DigitalInput(con=index, port=port)
self.available_channels.add(channel)

def add_octave(self, indices: Union[List[int], int]):
"""
Adds N octaves to the available instrument channels with the specified indices.
Expand Down
34 changes: 33 additions & 1 deletion qualang_tools/wirer/visualizer/instrument_figure_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
from matplotlib.axes import Axes
from matplotlib.figure import Figure

from qualang_tools.wirer.visualizer.layout import INSTRUMENT_FIGURE_DIMENSIONS, instrument_id_mapping
from qualang_tools.wirer.visualizer.layout import (
INSTRUMENT_FIGURE_DIMENSIONS,
_QDAC2_X0_NORM,
_QDAC2_X_SPAN_NORM,
instrument_id_mapping,
)


def _key(instrument_id: str, con: int):
Expand Down Expand Up @@ -32,6 +37,10 @@ def get_ax(self, con: int, slot: int, instrument_id: str) -> Axes:
fig = self._make_octave_figure()
self.figures[key] = fig.axes[0]
fig.suptitle(f"oct{con} - {instrument_id} Wiring", fontweight="bold", fontsize=14)
elif instrument_id == "QDAC2":
fig = self._make_qdac2_figure()
self.figures[key] = fig.axes[0]
fig.suptitle(f"qdac{con} - {instrument_id} Wiring", fontweight="bold", fontsize=14)
else:
fig = self._make_external_mixer_figure()
self.figures[key] = fig.axes[0]
Expand Down Expand Up @@ -87,6 +96,29 @@ def _make_opx_plus_figure() -> Figure:
def _make_octave_figure(cls) -> Figure:
return cls._make_opx_plus_figure()

@staticmethod
def _make_qdac2_figure() -> Figure:
fig, ax = plt.subplots(
1,
1,
figsize=(
INSTRUMENT_FIGURE_DIMENSIONS["QDAC2"]["width"] * 2,
INSTRUMENT_FIGURE_DIMENSIONS["QDAC2"]["height"] * 2,
),
)
ax.set_xlim([0.10 * 3, 1.0 * 3])
ax.set_ylim([0.02, 0.92])
ax.set_facecolor("darkgrey")
ax.set_xticks([])
ax.set_yticks([])
ax.set_xticklabels([])
ax.set_yticklabels([])
ax.set_aspect("equal")
_xc = (_QDAC2_X0_NORM + _QDAC2_X_SPAN_NORM / 2) * 3.0
ax.text(_xc, 0.87, "DC outputs 1-24", ha="center", va="center", fontsize=10, color="gainsboro")
ax.text(_xc, 0.055, "External triggers 1-4", ha="center", va="center", fontsize=9, color="gainsboro")
return fig

@classmethod
def _make_external_mixer_figure(cls) -> Figure:
fig, ax = plt.subplots(
Expand Down
27 changes: 27 additions & 0 deletions qualang_tools/wirer/visualizer/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"opx+": "OPX+",
"octave": "Octave",
"external-mixer": "Mixers",
"qdac2": "QDAC2",
}

# Define the chassis dimensions
Expand All @@ -13,6 +14,7 @@
"OPX+": {"width": 8, "height": 1},
"Octave": {"width": 3, "height": 1},
"Mixers": {"width": 1, "height": 1},
"QDAC2": {"width": 12, "height": 3},
}

OPX_PLUS_ASPECT = INSTRUMENT_FIGURE_DIMENSIONS["OPX+"]["height"] / INSTRUMENT_FIGURE_DIMENSIONS["OPX+"]["width"]
Expand All @@ -24,6 +26,27 @@
fem_analog_output_positions = [(0.05 + 0.25 * OPX_1000_ASPECT, 1.06 - i * PORT_SPACING_FACTOR) for i in range(8)]
fem_digital_output_positions = [(0.05 + 0.75 * OPX_1000_ASPECT, 0.86 - i * PORT_SPACING_FACTOR / 2.4) for i in range(8)]

# QDAC-II style: three front-panel rows of eight DC outputs (channels 1–24, top to bottom),
# plus a row of four external trigger inputs along the lower edge.
# X positions must stay inside the QDAC2 axes xlim (see instrument_figure_manager) so port labels
# (drawn slightly left of each port) are not clipped in the margin.
_QDAC2_X_SCALE = 3.0
_QDAC2_X0_NORM = 0.155 # left edge of 8-column grid (normalized, before * _QDAC2_X_SCALE)
_QDAC2_X_SPAN_NORM = 0.745 # (x_last - x_first) for the eight columns
qdac2_analog_output_positions = [
(
(_QDAC2_X0_NORM + col * (_QDAC2_X_SPAN_NORM / 7)) * _QDAC2_X_SCALE,
0.8 - row * 0.24,
)
for row in range(3)
for col in range(8)
]
_qdac2_trig_u0 = 0.22
_qdac2_trig_span = 0.52
qdac2_digital_input_positions = [
((_qdac2_trig_u0 + i * (_qdac2_trig_span / 3)) * _QDAC2_X_SCALE, 0.11) for i in range(4)
]

PORT_POSITIONS = {
"lf-fem": {
"analog": {
Expand Down Expand Up @@ -68,4 +91,8 @@
"input": [(0.25, 0.85)],
},
},
"qdac2": {
"analog": {"output": qdac2_analog_output_positions},
"digital": {"input": qdac2_digital_input_positions},
},
}
14 changes: 14 additions & 0 deletions qualang_tools/wirer/visualizer/port_annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ def draw(self, ax: Axes):
color=outline_colour,
)
bbox = None
elif self.instrument_id in ["qdac2"]:
port_size = PORT_SIZE * 0.72
port_label_distance = PORT_SIZE * 1.05
ax.text(
x - port_label_distance,
y,
str(self.port),
ha="center",
va="center",
fontsize=7,
fontweight="bold",
color=outline_colour,
)
bbox = None
else:
raise NotImplementedError(f"No port-annotation drawing for {self.instrument_id}")

Expand Down
25 changes: 25 additions & 0 deletions qualang_tools/wirer/wirer/channel_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
InstrumentChannelLfFemDigitalOutput,
InstrumentChannelOctaveDigitalInput,
InstrumentChannelExternalMixerDigitalInput,
InstrumentChannelQdac2Output,
InstrumentChannelQdac2DigitalInput,
)

# A channel template is a partially filled InstrumentChannel object
Expand Down Expand Up @@ -214,6 +216,28 @@ def __init__(self, index: int = None, rf_in: int = None, rf_out: int = None):
]


class ChannelSpecQdac2(ChannelSpec):
def __init__(self, index: int = None, out_port: int = None, trigger_in_port: int = None):
"""
Represents a DC voltage gate on a QDAC2 (analog output plus optional external trigger input).

Use with ``add_voltage_gate_lines`` (and related quantum-dot helpers). For a triggered line,
allocation picks one DC output and one digital trigger input on the same QDAC2 unit. For an
untriggered line, only the analog output is used.

Args:
index (int, optional): QDAC2 unit index (maps to ``con`` on the channel; any positive integer).
out_port (int, optional): DC analog output port (1-24; only used when allocating the output).
trigger_in_port (int, optional): External trigger input port (1-4; only used when the line
is triggered).
"""
super().__init__()
self.channel_templates = [
InstrumentChannelQdac2Output(con=index, port=out_port),
InstrumentChannelQdac2DigitalInput(con=index, port=trigger_in_port),
]


class ChannelSpecExternalMixer(ChannelSpec):
def __init__(self, index: int = None, rf_in: int = None, rf_out: int = None):
"""
Expand Down Expand Up @@ -515,6 +539,7 @@ def __init__(self, con: int = None, in_port: int = None):
opx_iq_ext_mixer_spec = ChannelSpecOpxPlusBasebandAndExternalMixer
octave_spec = ChannelSpecOctave
ext_mixer_spec = ChannelSpecExternalMixer
qdac2_spec = ChannelSpecQdac2
opx_dig_spec = ChannelSpecOpxPlusDigital
lf_fem_dig_spec = ChannelSpecLfFemDigital
mw_fem_dig_spec = ChannelSpecMwFemDigital
2 changes: 2 additions & 0 deletions qualang_tools/wirer/wirer/channel_specs_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
ChannelSpecOpxPlusBaseband,
ChannelSpecOpxPlusBasebandAndOctave,
ChannelSpecOctave,
ChannelSpecQdac2,
)

mw_fem_spec = ChannelSpecMwFemSingle
Expand All @@ -17,3 +18,4 @@
opx_iq_spec = ChannelSpecOpxPlusBaseband
opx_iq_octave_spec = ChannelSpecOpxPlusBasebandAndOctave
octave_spec = ChannelSpecOctave
qdac2_spec = ChannelSpecQdac2
21 changes: 20 additions & 1 deletion qualang_tools/wirer/wirer/wirer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ChannelSpecOctaveDigital,
ChannelSpecOpxPlusDigital,
ChannelSpecExternalMixerDigital,
ChannelSpecQdac2,
)
from .wirer_assign_channels_to_spec import assign_channels_to_spec
from .wirer_exceptions import ConstraintsTooStrictException, NotEnoughChannelsException
Expand Down Expand Up @@ -136,15 +137,33 @@ def _allocate_wiring(spec: WiringSpec, instruments: Instruments, observe_pulser_

def allocate_dc_channels(spec: WiringSpec, instruments: Instruments, observe_pulser_allocation: bool = False):
"""
Try to allocate DC channels to an LF-FEM or OPX+ to satisfy the spec.
Try to allocate DC channels to an LF-FEM, OPX+, and/or QDAC2 to satisfy the spec.

Single-instrument masks are tried first. If the (filtered) constraints mention both an LF-FEM and
QDAC2 (e.g. ``lf_fem_spec(...) & qdac2_spec(...)``), an extra LF-FEM+QDAC2 mask is appended so each
element receives one LF analog output (and digital if triggered) plus one QDAC2 output (and trigger
input if triggered). The same idea applies for OPX+ combined with QDAC2.
"""
dc_specs = [
# LF-FEM, Single analog output
ChannelSpecLfFemSingle() & ChannelSpecLfFemDigital(),
# OPX+, Single analog output
ChannelSpecOpxPlusSingle() & ChannelSpecOpxPlusDigital(),
# QDAC2, DC output + external trigger input (digital)
ChannelSpecQdac2(),
]

if spec.constraints:
filtered_c = spec.constraints.filter_by_wiring_spec(spec)
instr_ids = {t.instrument_id for t in filtered_c.channel_templates}
wants_lf = "lf-fem" in instr_ids
wants_opx = "opx+" in instr_ids
wants_qdac = "qdac2" in instr_ids
if wants_lf and wants_qdac:
dc_specs.append(ChannelSpecLfFemSingle() & ChannelSpecLfFemDigital() & ChannelSpecQdac2())
elif wants_opx and wants_qdac:
dc_specs.append(ChannelSpecOpxPlusSingle() & ChannelSpecOpxPlusDigital() & ChannelSpecQdac2())

allocate_channels(
spec, dc_specs, instruments, same_con=True, same_slot=True, observe_pulser_allocation=observe_pulser_allocation
)
Expand Down
11 changes: 11 additions & 0 deletions tests/wirer/test_instrument_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ def test_opx_plus_and_octave_validation():
instruments.add_octave(indices=1)


def test_opx_plus_and_qdac2_validation():
instruments = Instruments()
instruments.add_opx_plus(controllers=1)
instruments.add_qdac2(indices=1)


def test_redefinition_validation():
instruments = Instruments()
instruments.add_opx_plus(controllers=1)
Expand All @@ -22,6 +28,11 @@ def test_redefinition_validation():
with pytest.raises(ValueError):
instruments.add_octave(indices=1)

instruments = Instruments()
instruments.add_qdac2(indices=1)
with pytest.raises(ValueError):
instruments.add_qdac2(indices=1)


def test_slot_filled_validation():
instruments = Instruments()
Expand Down
Loading
Loading